Code splitting
- How to
- 5 minutes
When a user opens your app, the browser first downloads and parses main.js, the primary JavaScript bundle, before anything appears on screen. In large apps, this bundle can grow to several megabytes, causing slow initial load times. Keeping main.js below 1 MB is a good target; the smaller it is, the faster your app starts. Code splitting addresses this by breaking your app into smaller chunks that are loaded on demand, so the initial bundle contains only what is needed to render the first screen.
To reduce your app's main JavaScript bundle size:
- Enable the
--optimizeand--enable-code-splittingflags - Convert static route imports to lazy-loaded routes
- Split heavy components (optional)
- Load non-component code on demand (optional)
- Add loading feedback to Suspense boundaries
- Build and verify
Prerequisites
Code splitting requires the --optimize and --enable-code-splitting flags, which are available in the dt-app CLI version 1.9.0 or later. Both flags must be used together. The --optimize flag enables the new underlying bundler, which is required for --enable-code-splitting to take effect.
Steps
1. Enable code splitting in your build scripts
Run the build and deploy commands with both flags:
npx dt-app build --optimize --enable-code-splitting
npx dt-app deploy --optimize --enable-code-splitting
2. Convert static route imports to lazy-loaded routes
Route-based splitting is the most impactful technique for most apps. Each route loads only the code it needs, so users navigating to the home page never download the code for the data page, and vice versa.
Replace static import statements for your page components with React.lazy() and wrap your routes in a Suspense boundary:
import { Page } from "@dynatrace/strato-components-preview/layouts";
import React, { Suspense, lazy } from "react";
import { Route, Routes } from "react-router-dom";
import { Header } from "./components/Header";
import { ProgressCircle } from "@dynatrace/strato-components/content";
const Home = lazy(() => import("./pages/Home"));
const Data = lazy(() => import("./pages/Data"));
export const App = () => {
return (
<Page>
<Page.Header>
<Header />
</Page.Header>
<Page.Main>
<Suspense fallback={<ProgressCircle />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/data" element={<Data />} />
</Routes>
</Suspense>
</Page.Main>
</Page>
);
};
Each page component must use a default export:
const Home = () => {
return <div>Welcome To Your Dynatrace App</div>;
};
export default Home;
const Data = () => {
return <div>Explore Data</div>;
};
export default Data;
React.lazy() requires the dynamically imported module to have a default export containing a React component. If your pages use named exports, see the named exports section below.
Handle named exports
If your page components use named exports (for example, export const Home = () => { ... }), wrap the dynamic import to map the named export to a default export:
const Home = lazy(() => import('./pages/Home').then((module) => ({ default: module.Home })));
3. Split heavy components
You can also split individual components that are heavy or not immediately visible. This is useful for components like charts, editors, or modals that add significant JavaScript weight but are only needed after user interaction.
For example, if your Data page includes a heavy chart library, lazy-load just the chart:
import React, { Suspense, lazy } from "react";
import { Flex } from "@dynatrace/strato-components/layouts";
import { Heading } from "@dynatrace/strato-components/typography";
import { ProgressCircle } from "@dynatrace/strato-components/content";
const DataChart = lazy(() => import("../components/DataChart"));
const Data = () => {
return (
<Flex flexDirection="column" padding={32}>
<Heading level={2}>Explore Data</Heading>
<Suspense fallback={<ProgressCircle />}>
<DataChart />
</Suspense>
</Flex>
);
};
export default Data;
4. Load non-component code on demand
For non-component code such as utility functions or data processing libraries, use dynamic import() directly. This gives you fine-grained control over when a module is loaded.
import React, { useState } from 'react';
const Data = () => {
const [result, setResult] = useState(null);
const handleAnalyze = () => {
void import('../utils/analyze').then(({ analyzeData }) => {
setResult(analyzeData());
});
};
return (
<div>
<button onClick={handleAnalyze}>Analyze</button>
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</div>
);
};
export default Data;
The ../utils/analyze module is only downloaded when the user clicks the button.
5. Add loading feedback to Suspense boundaries
Every Suspense boundary needs a fallback that shows while the chunk loads. Use Strato's ProgressCircle to give users visual feedback instead of a plain text placeholder. For example, the MapView component is large and benefits from a spinner while it loads:
import React, { Suspense, lazy } from 'react';
import { Flex } from '@dynatrace/strato-components/layouts';
import { ProgressCircle } from '@dynatrace/strato-components/content';
const MapView = lazy(() => import('@dynatrace/strato-geo').then((mod) => ({ default: mod.MapView })));
const Map = () => {
return (
<Flex flexDirection="column" padding={32}>
<Suspense fallback={<ProgressCircle />}>
<MapView />
</Suspense>
</Flex>
);
};
export default Map;
6. Build and verify
Build the app to confirm code splitting works:
npx dt-app build --optimize --enable-code-splitting
The build command creates a dist folder containing the compiled output. You should see multiple .js files instead of a single bundle, confirming that code splitting is active.
Summary
| Technique | Best for | How it works |
|---|---|---|
| Route-based splitting | Pages / views | React.lazy() + Suspense on route components |
| Component-based splitting | Heavy components | React.lazy() + Suspense on individual components |
Dynamic import() | Utility code, libraries | await import() triggered by user action |
Combine these techniques to progressively reduce your app's initial bundle size and improve load performance.