Skip to main content

portalis_transpiler/
code_splitter.rs

1//! Code Splitter - Splits WASM modules for lazy loading and optimization
2//!
3//! This module provides:
4//! 1. Module splitting analysis and recommendations
5//! 2. Lazy loading strategy for WASM modules
6//! 3. Dynamic import detection and optimization
7//! 4. Chunk size analysis and balancing
8//! 5. Split point detection based on usage patterns
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Code splitting strategy
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum SplittingStrategy {
16    /// No splitting - single monolithic bundle
17    None,
18    /// Split by route/page (for web apps)
19    ByRoute,
20    /// Split by feature (feature-based chunking)
21    ByFeature,
22    /// Split by lazy loading boundaries
23    ByLazyLoad,
24    /// Split by size threshold
25    BySize,
26    /// Automatic splitting based on analysis
27    Automatic,
28}
29
30/// Split point in the code
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SplitPoint {
33    /// Split point identifier
34    pub id: String,
35    /// Module name
36    pub module: String,
37    /// Function or boundary name
38    pub boundary: String,
39    /// Estimated size of chunk (bytes)
40    pub estimated_size: u64,
41    /// Loading priority (0 = critical, 1 = high, 2 = medium, 3 = low)
42    pub priority: u8,
43    /// Dependencies needed by this chunk
44    pub dependencies: Vec<String>,
45    /// Can be lazy loaded
46    pub lazy_loadable: bool,
47}
48
49/// Code chunk after splitting
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct CodeChunk {
52    /// Chunk identifier
53    pub id: String,
54    /// Chunk name
55    pub name: String,
56    /// Modules included in this chunk
57    pub modules: Vec<String>,
58    /// Functions included
59    pub functions: Vec<String>,
60    /// Estimated size (bytes)
61    pub size: u64,
62    /// Loading priority
63    pub priority: u8,
64    /// Chunks this depends on
65    pub dependencies: Vec<String>,
66    /// Is this an entry point chunk
67    pub is_entry: bool,
68}
69
70impl CodeChunk {
71    /// Check if chunk should be eagerly loaded
72    pub fn should_preload(&self) -> bool {
73        self.is_entry || self.priority == 0
74    }
75
76    /// Get loading strategy
77    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/// Code splitting analysis result
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SplittingAnalysis {
93    /// Original bundle size
94    pub original_size: u64,
95    /// Recommended chunks
96    pub chunks: Vec<CodeChunk>,
97    /// Split points identified
98    pub split_points: Vec<SplitPoint>,
99    /// Estimated initial load size (critical chunks only)
100    pub initial_load_size: u64,
101    /// Total size after splitting (including overhead)
102    pub total_split_size: u64,
103    /// Splitting strategy used
104    pub strategy: SplittingStrategy,
105}
106
107impl SplittingAnalysis {
108    /// Calculate size reduction percentage
109    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    /// Calculate overhead from splitting
117    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    /// Generate detailed report
126    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        // Categorize chunks by loading strategy
149        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/// Lazy loading configuration
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct LazyLoadConfig {
202    /// Enable lazy loading
203    pub enabled: bool,
204    /// Size threshold for creating separate chunk (bytes)
205    pub size_threshold: u64,
206    /// Maximum number of chunks
207    pub max_chunks: usize,
208    /// Minimum chunk size (bytes)
209    pub min_chunk_size: u64,
210    /// Enable route-based splitting
211    pub route_based: bool,
212}
213
214impl Default for LazyLoadConfig {
215    fn default() -> Self {
216        Self {
217            enabled: true,
218            size_threshold: 50 * 1024, // 50 KB
219            max_chunks: 20,
220            min_chunk_size: 10 * 1024, // 10 KB
221            route_based: false,
222        }
223    }
224}
225
226impl LazyLoadConfig {
227    /// Create aggressive splitting config
228    pub fn aggressive() -> Self {
229        Self {
230            enabled: true,
231            size_threshold: 30 * 1024, // 30 KB
232            max_chunks: 50,
233            min_chunk_size: 5 * 1024, // 5 KB
234            route_based: true,
235        }
236    }
237
238    /// Create conservative splitting config
239    pub fn conservative() -> Self {
240        Self {
241            enabled: true,
242            size_threshold: 100 * 1024, // 100 KB
243            max_chunks: 10,
244            min_chunk_size: 20 * 1024, // 20 KB
245            route_based: false,
246        }
247    }
248}
249
250/// Code splitter
251pub struct CodeSplitter {
252    config: LazyLoadConfig,
253    strategy: SplittingStrategy,
254}
255
256impl CodeSplitter {
257    /// Create new code splitter
258    pub fn new(strategy: SplittingStrategy) -> Self {
259        Self {
260            config: LazyLoadConfig::default(),
261            strategy,
262        }
263    }
264
265    /// Create with custom config
266    pub fn with_config(strategy: SplittingStrategy, config: LazyLoadConfig) -> Self {
267        Self { config, strategy }
268    }
269
270    /// Analyze code for splitting opportunities
271    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    /// No splitting - return single chunk
284    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    /// Split by size threshold
305    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        // Entry chunk (always loaded)
311        let mut entry_modules = Vec::new();
312        let mut entry_size = 0u64;
313
314        // Lazy chunks
315        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            // Critical modules go in entry chunk
321            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                // Create new chunk
326                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        // Add remaining chunk
360        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        // Add entry chunk at the beginning
374        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); // Overhead per chunk
387
388        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    /// Split by feature
399    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        // Categorize modules by feature
405        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        // Core chunk (entry)
426        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        // UI chunk (preload)
438        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        // Data chunk (lazy)
462        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    /// Automatic splitting with intelligent analysis
499    fn auto_split(&self, modules: &HashMap<String, u64>) -> SplittingAnalysis {
500        let original_size: u64 = modules.values().sum();
501
502        // Use size-based splitting as the automatic strategy
503        if original_size > 500 * 1024 {
504            // Large bundle - use aggressive splitting
505            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            // Medium bundle - use default splitting
512            self.split_by_size(modules)
513        } else {
514            // Small bundle - no splitting needed
515            self.no_splitting(original_size)
516        }
517    }
518
519    /// Check if module is critical (must be in entry chunk)
520    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    /// Generate dynamic import code
528    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    /// Generate webpack config for code splitting
546    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        // Core should be in entry chunk
624        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}