Lazy loading quickstart
- How-to guide
- 5-min read
Code splitting breaks an app into smaller chunks so only the necessary code is loaded initially, reducing the upfront bundle size. Lazy loading then delays loading those chunks until they're actually needed, delivering the real performance gains by fetching code on demand.
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.
Here are the essential steps to enable code splitting in your Dynatrace app. For a more thorough explanation, see Understand lazy loading.
Prerequisites
dt-appCLI version 1.9.0 or higher.
Steps
1. 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/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 })));
2. 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 and add a button to trigger loading it:
import React, { Suspense, lazy, useState } from 'react';
import { Button } from '@dynatrace/strato-components/buttons';
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'));
export const Data = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<Flex flexDirection="column" padding={32}>
<Heading level={2}>Explore Data</Heading>
<Button onClick={() => setIsOpen(true)}>Explore Data</Button>
{isOpen && (
<Modal title="Explore Data" show onDismiss={() => setIsOpen(false)}>
<Suspense fallback={<ProgressCircle />}>
<DataChart onClose={() => setIsOpen(false)} />
</Suspense>
</Modal>
)}
</Flex>
);
};
3. 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.
4. 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;
5. Build and verify
Build and deploy the app to confirm code splitting works by enabling code splitting in your build scripts:
npx dt-app build --optimize --enable-code-splitting
npx dt-app deploy --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 |
Combine these techniques to progressively reduce your app's initial bundle size and improve load performance.