1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum SplittingStrategy {
16 None,
18 ByRoute,
20 ByFeature,
22 ByLazyLoad,
24 BySize,
26 Automatic,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SplitPoint {
33 pub id: String,
35 pub module: String,
37 pub boundary: String,
39 pub estimated_size: u64,
41 pub priority: u8,
43 pub dependencies: Vec<String>,
45 pub lazy_loadable: bool,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct CodeChunk {
52 pub id: String,
54 pub name: String,
56 pub modules: Vec<String>,
58 pub functions: Vec<String>,
60 pub size: u64,
62 pub priority: u8,
64 pub dependencies: Vec<String>,
66 pub is_entry: bool,
68}
69
70impl CodeChunk {
71 pub fn should_preload(&self) -> bool {
73 self.is_entry || self.priority == 0
74 }
75
76 pub fn loading_strategy(&self) -> &str {
78 if self.is_entry {
79 "eager"
80 } else if self.priority == 0 {
81 "preload"
82 } else if self.priority == 1 {
83 "prefetch"
84 } else {
85 "lazy"
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SplittingAnalysis {
93 pub original_size: u64,
95 pub chunks: Vec<CodeChunk>,
97 pub split_points: Vec<SplitPoint>,
99 pub initial_load_size: u64,
101 pub total_split_size: u64,
103 pub strategy: SplittingStrategy,
105}
106
107impl SplittingAnalysis {
108 pub fn size_reduction_percent(&self) -> f64 {
110 if self.original_size == 0 {
111 return 0.0;
112 }
113 ((self.original_size - self.initial_load_size) as f64 / self.original_size as f64) * 100.0
114 }
115
116 pub fn splitting_overhead(&self) -> u64 {
118 if self.total_split_size > self.original_size {
119 self.total_split_size - self.original_size
120 } else {
121 0
122 }
123 }
124
125 pub fn generate_report(&self) -> String {
127 let mut report = String::new();
128
129 report.push_str("=== Code Splitting Analysis ===\n\n");
130 report.push_str(&format!("Strategy: {:?}\n", self.strategy));
131 report.push_str(&format!("Original Size: {}\n", Self::format_size(self.original_size)));
132 report.push_str(&format!("Initial Load Size: {} ({:.1}% of original)\n",
133 Self::format_size(self.initial_load_size),
134 (self.initial_load_size as f64 / self.original_size as f64) * 100.0
135 ));
136 report.push_str(&format!("Total Split Size: {}\n", Self::format_size(self.total_split_size)));
137
138 let overhead = self.splitting_overhead();
139 if overhead > 0 {
140 report.push_str(&format!("Splitting Overhead: {} ({:.1}%)\n",
141 Self::format_size(overhead),
142 (overhead as f64 / self.original_size as f64) * 100.0
143 ));
144 }
145
146 report.push_str(&format!("\nChunks Generated: {}\n", self.chunks.len()));
147
148 let mut eager = 0;
150 let mut preload = 0;
151 let mut prefetch = 0;
152 let mut lazy = 0;
153
154 for chunk in &self.chunks {
155 match chunk.loading_strategy() {
156 "eager" => eager += 1,
157 "preload" => preload += 1,
158 "prefetch" => prefetch += 1,
159 "lazy" => lazy += 1,
160 _ => {}
161 }
162 }
163
164 report.push_str(&format!(" - Eager: {} chunks\n", eager));
165 report.push_str(&format!(" - Preload: {} chunks\n", preload));
166 report.push_str(&format!(" - Prefetch: {} chunks\n", prefetch));
167 report.push_str(&format!(" - Lazy: {} chunks\n", lazy));
168
169 report.push_str("\nChunk Details:\n");
170 for chunk in &self.chunks {
171 report.push_str(&format!(" {} ({}) - {} - {} modules, {}\n",
172 chunk.name,
173 chunk.loading_strategy(),
174 Self::format_size(chunk.size),
175 chunk.modules.len(),
176 if chunk.is_entry { "ENTRY" } else { "" }
177 ));
178 }
179
180 report.push_str(&format!("\nSplit Points Identified: {}\n", self.split_points.len()));
181 for sp in self.split_points.iter().take(5) {
182 report.push_str(&format!(" - {} ({})\n", sp.boundary, sp.module));
183 }
184
185 report
186 }
187
188 fn format_size(bytes: u64) -> String {
189 if bytes < 1024 {
190 format!("{} B", bytes)
191 } else if bytes < 1024 * 1024 {
192 format!("{:.1} KB", bytes as f64 / 1024.0)
193 } else {
194 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct LazyLoadConfig {
202 pub enabled: bool,
204 pub size_threshold: u64,
206 pub max_chunks: usize,
208 pub min_chunk_size: u64,
210 pub route_based: bool,
212}
213
214impl Default for LazyLoadConfig {
215 fn default() -> Self {
216 Self {
217 enabled: true,
218 size_threshold: 50 * 1024, max_chunks: 20,
220 min_chunk_size: 10 * 1024, route_based: false,
222 }
223 }
224}
225
226impl LazyLoadConfig {
227 pub fn aggressive() -> Self {
229 Self {
230 enabled: true,
231 size_threshold: 30 * 1024, max_chunks: 50,
233 min_chunk_size: 5 * 1024, route_based: true,
235 }
236 }
237
238 pub fn conservative() -> Self {
240 Self {
241 enabled: true,
242 size_threshold: 100 * 1024, max_chunks: 10,
244 min_chunk_size: 20 * 1024, route_based: false,
246 }
247 }
248}
249
250pub struct CodeSplitter {
252 config: LazyLoadConfig,
253 strategy: SplittingStrategy,
254}
255
256impl CodeSplitter {
257 pub fn new(strategy: SplittingStrategy) -> Self {
259 Self {
260 config: LazyLoadConfig::default(),
261 strategy,
262 }
263 }
264
265 pub fn with_config(strategy: SplittingStrategy, config: LazyLoadConfig) -> Self {
267 Self { config, strategy }
268 }
269
270 pub fn analyze(&self, modules: &HashMap<String, u64>) -> SplittingAnalysis {
272 let original_size: u64 = modules.values().sum();
273
274 match self.strategy {
275 SplittingStrategy::None => self.no_splitting(original_size),
276 SplittingStrategy::BySize => self.split_by_size(modules),
277 SplittingStrategy::ByFeature => self.split_by_feature(modules),
278 SplittingStrategy::Automatic => self.auto_split(modules),
279 _ => self.split_by_size(modules),
280 }
281 }
282
283 fn no_splitting(&self, size: u64) -> SplittingAnalysis {
285 SplittingAnalysis {
286 original_size: size,
287 chunks: vec![CodeChunk {
288 id: "main".to_string(),
289 name: "main".to_string(),
290 modules: vec!["*".to_string()],
291 functions: vec![],
292 size,
293 priority: 0,
294 dependencies: vec![],
295 is_entry: true,
296 }],
297 split_points: vec![],
298 initial_load_size: size,
299 total_split_size: size,
300 strategy: SplittingStrategy::None,
301 }
302 }
303
304 fn split_by_size(&self, modules: &HashMap<String, u64>) -> SplittingAnalysis {
306 let original_size: u64 = modules.values().sum();
307 let mut chunks = Vec::new();
308 let mut split_points = Vec::new();
309
310 let mut entry_modules = Vec::new();
312 let mut entry_size = 0u64;
313
314 let mut current_chunk_modules = Vec::new();
316 let mut current_chunk_size = 0u64;
317 let mut chunk_counter = 1;
318
319 for (module, &size) in modules {
320 if Self::is_critical_module(module) {
322 entry_modules.push(module.clone());
323 entry_size += size;
324 } else if current_chunk_size + size > self.config.size_threshold {
325 if !current_chunk_modules.is_empty() {
327 chunks.push(CodeChunk {
328 id: format!("chunk_{}", chunk_counter),
329 name: format!("lazy_{}", chunk_counter),
330 modules: current_chunk_modules.clone(),
331 functions: vec![],
332 size: current_chunk_size,
333 priority: 2,
334 dependencies: vec!["main".to_string()],
335 is_entry: false,
336 });
337
338 split_points.push(SplitPoint {
339 id: format!("split_{}", chunk_counter),
340 module: module.clone(),
341 boundary: format!("chunk_{}_boundary", chunk_counter),
342 estimated_size: current_chunk_size,
343 priority: 2,
344 dependencies: vec!["main".to_string()],
345 lazy_loadable: true,
346 });
347
348 chunk_counter += 1;
349 }
350
351 current_chunk_modules = vec![module.clone()];
352 current_chunk_size = size;
353 } else {
354 current_chunk_modules.push(module.clone());
355 current_chunk_size += size;
356 }
357 }
358
359 if !current_chunk_modules.is_empty() {
361 chunks.push(CodeChunk {
362 id: format!("chunk_{}", chunk_counter),
363 name: format!("lazy_{}", chunk_counter),
364 modules: current_chunk_modules,
365 functions: vec![],
366 size: current_chunk_size,
367 priority: 2,
368 dependencies: vec!["main".to_string()],
369 is_entry: false,
370 });
371 }
372
373 chunks.insert(0, CodeChunk {
375 id: "main".to_string(),
376 name: "main".to_string(),
377 modules: entry_modules,
378 functions: vec![],
379 size: entry_size,
380 priority: 0,
381 dependencies: vec![],
382 is_entry: true,
383 });
384
385 let total_split_size = chunks.iter().map(|c| c.size).sum::<u64>() +
386 (chunks.len() as u64 * 100); SplittingAnalysis {
389 original_size,
390 chunks,
391 split_points,
392 initial_load_size: entry_size,
393 total_split_size,
394 strategy: SplittingStrategy::BySize,
395 }
396 }
397
398 fn split_by_feature(&self, modules: &HashMap<String, u64>) -> SplittingAnalysis {
400 let original_size: u64 = modules.values().sum();
401 let mut chunks = Vec::new();
402 let mut split_points = Vec::new();
403
404 let mut core_modules = Vec::new();
406 let mut core_size = 0u64;
407 let mut ui_modules = Vec::new();
408 let mut ui_size = 0u64;
409 let mut data_modules = Vec::new();
410 let mut data_size = 0u64;
411
412 for (module, &size) in modules {
413 if module.contains("ui") || module.contains("component") {
414 ui_modules.push(module.clone());
415 ui_size += size;
416 } else if module.contains("data") || module.contains("api") {
417 data_modules.push(module.clone());
418 data_size += size;
419 } else {
420 core_modules.push(module.clone());
421 core_size += size;
422 }
423 }
424
425 chunks.push(CodeChunk {
427 id: "core".to_string(),
428 name: "core".to_string(),
429 modules: core_modules,
430 functions: vec![],
431 size: core_size,
432 priority: 0,
433 dependencies: vec![],
434 is_entry: true,
435 });
436
437 if !ui_modules.is_empty() {
439 chunks.push(CodeChunk {
440 id: "ui".to_string(),
441 name: "ui".to_string(),
442 modules: ui_modules,
443 functions: vec![],
444 size: ui_size,
445 priority: 1,
446 dependencies: vec!["core".to_string()],
447 is_entry: false,
448 });
449
450 split_points.push(SplitPoint {
451 id: "ui_split".to_string(),
452 module: "ui".to_string(),
453 boundary: "ui_boundary".to_string(),
454 estimated_size: ui_size,
455 priority: 1,
456 dependencies: vec!["core".to_string()],
457 lazy_loadable: true,
458 });
459 }
460
461 if !data_modules.is_empty() {
463 chunks.push(CodeChunk {
464 id: "data".to_string(),
465 name: "data".to_string(),
466 modules: data_modules,
467 functions: vec![],
468 size: data_size,
469 priority: 2,
470 dependencies: vec!["core".to_string()],
471 is_entry: false,
472 });
473
474 split_points.push(SplitPoint {
475 id: "data_split".to_string(),
476 module: "data".to_string(),
477 boundary: "data_boundary".to_string(),
478 estimated_size: data_size,
479 priority: 2,
480 dependencies: vec!["core".to_string()],
481 lazy_loadable: true,
482 });
483 }
484
485 let total_split_size = chunks.iter().map(|c| c.size).sum::<u64>() +
486 (chunks.len() as u64 * 100);
487
488 SplittingAnalysis {
489 original_size,
490 chunks,
491 split_points,
492 initial_load_size: core_size,
493 total_split_size,
494 strategy: SplittingStrategy::ByFeature,
495 }
496 }
497
498 fn auto_split(&self, modules: &HashMap<String, u64>) -> SplittingAnalysis {
500 let original_size: u64 = modules.values().sum();
501
502 if original_size > 500 * 1024 {
504 let splitter = Self::with_config(
506 SplittingStrategy::BySize,
507 LazyLoadConfig::aggressive(),
508 );
509 splitter.split_by_size(modules)
510 } else if original_size > 200 * 1024 {
511 self.split_by_size(modules)
513 } else {
514 self.no_splitting(original_size)
516 }
517 }
518
519 fn is_critical_module(module: &str) -> bool {
521 module == "main" ||
522 module.contains("init") ||
523 module.contains("bootstrap") ||
524 module.contains("core")
525 }
526
527 pub fn generate_dynamic_import(&self, chunk: &CodeChunk) -> String {
529 format!(
530 r#"
531// Dynamic import for chunk: {}
532async fn load_{}() -> Result<(), JsValue> {{
533 wasm_bindgen_futures::JsFuture::from(
534 js_sys::eval(&format!(
535 "import('./pkg/{}.js')",
536 )).unwrap()
537 ).await?;
538 Ok(())
539}}
540"#,
541 chunk.name, chunk.id, chunk.name
542 )
543 }
544
545 pub fn generate_webpack_config(&self, analysis: &SplittingAnalysis) -> String {
547 let mut config = String::new();
548
549 config.push_str("module.exports = {\n");
550 config.push_str(" optimization: {\n");
551 config.push_str(" splitChunks: {\n");
552 config.push_str(" chunks: 'all',\n");
553 config.push_str(&format!(" maxSize: {},\n", self.config.size_threshold));
554 config.push_str(&format!(" minSize: {},\n", self.config.min_chunk_size));
555 config.push_str(" cacheGroups: {\n");
556
557 for chunk in &analysis.chunks {
558 if !chunk.is_entry {
559 config.push_str(&format!(" {}: {{\n", chunk.id));
560 config.push_str(&format!(" name: '{}',\n", chunk.name));
561 config.push_str(&format!(" priority: {},\n", chunk.priority));
562 config.push_str(" reuseExistingChunk: true,\n");
563 config.push_str(" },\n");
564 }
565 }
566
567 config.push_str(" },\n");
568 config.push_str(" },\n");
569 config.push_str(" },\n");
570 config.push_str("};\n");
571
572 config
573 }
574}
575
576impl Default for CodeSplitter {
577 fn default() -> Self {
578 Self::new(SplittingStrategy::Automatic)
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn test_no_splitting() {
588 let splitter = CodeSplitter::new(SplittingStrategy::None);
589 let mut modules = HashMap::new();
590 modules.insert("main".to_string(), 100_000);
591
592 let analysis = splitter.analyze(&modules);
593
594 assert_eq!(analysis.chunks.len(), 1);
595 assert_eq!(analysis.initial_load_size, 100_000);
596 }
597
598 #[test]
599 fn test_split_by_size() {
600 let splitter = CodeSplitter::new(SplittingStrategy::BySize);
601 let mut modules = HashMap::new();
602 modules.insert("main".to_string(), 40_000);
603 modules.insert("module_a".to_string(), 60_000);
604 modules.insert("module_b".to_string(), 80_000);
605
606 let analysis = splitter.analyze(&modules);
607
608 assert!(analysis.chunks.len() > 1);
609 assert!(analysis.initial_load_size < analysis.original_size);
610 }
611
612 #[test]
613 fn test_split_by_feature() {
614 let splitter = CodeSplitter::new(SplittingStrategy::ByFeature);
615 let mut modules = HashMap::new();
616 modules.insert("core".to_string(), 50_000);
617 modules.insert("ui_component".to_string(), 30_000);
618 modules.insert("data_api".to_string(), 40_000);
619
620 let analysis = splitter.analyze(&modules);
621
622 assert!(analysis.chunks.len() >= 2);
623 assert!(analysis.chunks.iter().any(|c| c.is_entry));
625 }
626
627 #[test]
628 fn test_lazy_load_config() {
629 let aggressive = LazyLoadConfig::aggressive();
630 let conservative = LazyLoadConfig::conservative();
631
632 assert!(aggressive.size_threshold < conservative.size_threshold);
633 assert!(aggressive.max_chunks > conservative.max_chunks);
634 }
635
636 #[test]
637 fn test_chunk_loading_strategy() {
638 let entry_chunk = CodeChunk {
639 id: "main".to_string(),
640 name: "main".to_string(),
641 modules: vec![],
642 functions: vec![],
643 size: 1000,
644 priority: 0,
645 dependencies: vec![],
646 is_entry: true,
647 };
648
649 assert_eq!(entry_chunk.loading_strategy(), "eager");
650 assert!(entry_chunk.should_preload());
651 }
652}