A deep dive into modernizing a massive React 15 + webpack 1 codebase while maintaining legacy browser support, covering codemods, differential serving, Preact migration, and jQuery removal strategies.
Matheus Albuquerque, Staff Front-End Engineer at Medallia, shares the challenges and solutions for optimizing their massive Customer Experience (CX) platform that serves millions of survey takers across thousands of tenants.
The Legacy Challenge
The platform was running on React 15, Node 10, and webpack 1 - a combination that posed significant performance and maintenance challenges. The team needed to modernize while supporting legacy browsers including Internet Explorer 10.
Phase 1: Modernizing Dependencies
The Migration Strategy
The team faced several critical decisions when upgrading their dependency stack:
Webpack Migration Complexity
- Moving from webpack 1 to 5 required sequential upgrades (V1→V2→V3→V4→V5)
- Plugin compatibility became a major issue - plugins needed to work together across versions
- Node.js requirements changed throughout the upgrade path
- Node fibers deprecation in Node 14+ broke several dependencies including Node-sass
React 15 to 16 Migration
- React 16 offered 32% bundle size reduction when combined with React-dom
- Prop-types moved from being part of React package to a separate dependency
- The team used jscodeshift with react-codemods for automated migration
- Feature flags were essential to roll out changes gradually to 3,000+ tenants
Code Generation Pipeline
The team built an automated code generation system:
- Created "next" wrapper packages behind feature flags
- Used jscodeshift transformers for React components and utilities
- Generated new webpack and Babel configurations
- Automated gitignore file generation to avoid duplication
- Used WebDriver codemods for test migration (V4→V7)
Phase 2: Code Splitting
Initial Implementation
- Started with React.lazy and Suspense for dynamic imports
- Split survey questions into separate chunks based on usage
- Achieved significant initial performance gains
Challenges and Solutions
- Client-written JavaScript/CSS broke when DOM elements weren't available
- Required sandboxing and orchestration strategies
- Scripts needed to wait until all components were loaded before execution
Phase 3: Preact Migration
Bundle Size Impact
- Preact + compat layer reduced bundle from 205KB to 175KB
- 30KB savings from switching from React to Preact
- Preact's lighter event system and lack of legacy code contributed to savings
Compatibility Issues
- Encountered rendering order problems with Preact 10 + code splitting
- Issues were non-deterministic and difficult to debug
- Resolved by switching from React.lazy/Suspense to loadable-components
- Loadable-components added only 2KB overhead
Phase 4: jQuery Removal
The Problem
- jQuery was globally available for client customizations
- Added unnecessary bundle weight
- Older jQuery versions had security vulnerabilities (CVEs)
Detection and Migration
- Built static analysis pipeline using Babel
- Analyzed hundreds of client customizations
- Used proxies for runtime detection of jQuery usage
- Created migration paths based on actual usage patterns
- Considered automated jQuery-to-native API translation (not deployed)
Phase 5: Browser Targeting Strategy
The Challenge
- Needed to support modern browsers while maintaining IE10 compatibility
- Lighthouse warnings about serving legacy JavaScript to modern browsers
- Different user agent behaviors across browsers
Polyfill Strategies Evaluated
Polyfill.io
- Pros: Easy setup, only serves necessary polyfills
- Cons: External dependency, security risks (supply chain attack), performance overhead
Babel Preset-env
- Entry mode: Safe but may not help very old browsers
- Usage mode: More aggressive but can cause runtime errors in legacy browsers
Differential Serving
- Serve two bundles: modern (ES modules) and legacy (ES5)
- Use nomodule attribute and type="module" for browser targeting
- Complex browser compatibility issues
Final Solution
- Implemented runtime detection using DOM script tags
- Created feature detection mechanism to choose appropriate bundle
- Avoided server-side user agent detection due to infrastructure constraints
Results
Bundle Size Reduction
- Modern browsers (ES modules supported): 37% reduction (280KB → 176KB)
- Legacy browsers (IE10): 25% reduction (280KB → 223KB)
- Only 0.2% of users received the larger legacy bundle
Performance Improvements
- Core Web Vitals improved across the board
- FCP, Speed Index, LCP, and TTI all improved by several seconds
- Visual completeness metrics showed measurable improvements
- Overall page build progress accelerated
Future Initiatives
The team continues to explore additional optimizations:
- Performance culture and tooling (IDE extensions, bundle analysis)
- Windowing for large dropdowns (6,000+ items)
- Advanced compression strategies (Zopfli, Guetzli, Zstandard)
- QUIC protocol for HTTP/3
- Partytown for offloading third-party scripts
Key Takeaways
Technical Insights
- Understanding compiler internals and static analysis is crucial for large-scale migrations
- jscodeshift and codemods are powerful tools for automated codebase transformations
- Feature flags are essential for gradual rollout of breaking changes
- Runtime detection can solve problems when infrastructure constraints exist
Strategic Lessons
- Always correlate performance metrics with business outcomes
- Test on real devices and networks, not just fast development environments
- The performance inequality gap is real - optimize for global conditions
- No silver bullet exists - solutions must be tailored to specific constraints
Cultural Impact
- Performance optimization requires organizational buy-in
- Tooling and culture are as important as technical solutions
- Continuous improvement mindset is essential for long-term success
The migration demonstrates how large organizations can modernize legacy codebases while maintaining compatibility and gradually improving performance for all users.

Comments
Please log in or register to join the discussion