The Problem: 15 Seconds to Load
A frontend app that already had lazy loading, preloading, and caching still took 15 seconds to load. The author discovered that the usual optimizations weren't enough. The app loaded too much code upfront, preloading made things slower, and cache died after every deployment.
The fix? Treat loading as part of the app architecture, not a bundler setting. The result: load time dropped to 1.1 seconds.
Stop Loading Unused Code
Lazy loading is essential but often misapplied. The app had 22 MB of static files. Without lazy loading, every page required downloading the entire application. After proper lazy loading, initial download dropped to 0.5 MB, with individual pages loading 1.1–3.5 MB.
Most developers only lazy-load pages. But the author argues for a second level: lazy-load everything not needed initially. Hidden UI components, rare logic, or even entire NPM packages should be deferred. In their case, this "beyond-pages" optimization accounted for 20% of the improvement.
> Rule: Load code only when there's a reason.
Don't Centralize What Should Load Separately
Dynamic imports are only part of the story. Static imports can undermine lazy loading. Consider this code:
// App.tsx
const Page1 = React.lazy(() => import('./pages/Page1'));
const Page2 = React.lazy(() => import('./pages/Page2'));
// Page1.tsx
import { HeavyChart } from './HeavyChart';
import { LargeTable } from './LargeTable';
export const CommonComponent = () => Common component;
export default function Page1() {
return (
<>
);
}
// Page2.tsx
import { CommonComponent } from './Page1';
import { Page2Content } from './Page2Content';
export default function Page2() {
return (
<>
);
}
Both pages are lazy-loaded, but Page2 imports CommonComponent from Page1. This forces the browser to download HeavyChart and LargeTable when opening Page2. Because JavaScript modules are file-based, importing anything from a file pulls in all its static dependencies.
The fix: move shared components into separate files. The author recommends one export per file for optimal performance, though pragmatic trade-offs are acceptable.
Make Your Cache Survive Deployments
Lazy loading helps the first visit; cache helps subsequent ones. But if file names change after every deployment, cache is lost. The solution: use content-based hashes.
// Webpack config
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
}
Even with content hashes, a single change can rename multiple files if chunks are interdependent. Clean file structure minimizes this.
Vendor splitting also matters. Putting all NPM packages into one vendor file is fragile—any change renames it, destroying cache. Instead, create cache groups only for stable, broadly-used libraries like React:
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
react: {
filename: `react.[contenthash:8].js`,
chunks: 'initial',
test: /react/,
},
},
},
},
};
This groups React-related packages into one initial chunk, while other packages can be split by actual usage.
Don't Make Users Wait Twice
Lazy loading moves the wait to later. To reduce that, preload future files and data early.
Use HTML hints:
Prefetch lazy files for UX-critical pages. Avoid preload unless you fully understand the trade-offs—misuse can hurt performance.
Also, use user actions as signals. If a user hovers a link, start loading that page's files before the click. Similarly, start fetching API data together with the lazy JavaScript, not after. In the author's case, this strategy alone saved about half of the 15-second improvement.
Conclusion
Lazy loading isn't enough. You must also:
- Lazy-load everything not needed initially, not just pages.
- Keep shared logic in separate files to avoid unwanted dependencies.
- Use content hashes and selective vendor groups for cache longevity.
- Preload files and data based on user signals.
These architectural changes turned a 15-second load into 1.1 seconds. Start by auditing your import structure—it's the biggest win.


