Skip to main content

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:

  1. Enable the --optimize and --enable-code-splitting flags
  2. Convert static route imports to lazy-loaded routes
  3. Split heavy components (optional)
  4. Load non-component code on demand (optional)
  5. Add loading feedback to Suspense boundaries
  6. 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:

ui/app/App.tsx
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:

ui/app/pages/Home.tsx
const Home = () => {
return <div>Welcome To Your Dynatrace App</div>;
};

export default Home;
ui/app/pages/Data.tsx
const Data = () => {
return <div>Explore Data</div>;
};

export default Data;
Note

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:

ui/app/App.tsx
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:

ui/app/pages/Data.tsx
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.

ui/app/pages/Data.tsx
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:

ui/app/pages/Map.tsx
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

TechniqueBest forHow it works
Route-based splittingPages / viewsReact.lazy() + Suspense on route components
Component-based splittingHeavy componentsReact.lazy() + Suspense on individual components
Dynamic import()Utility code, librariesawait import() triggered by user action

Combine these techniques to progressively reduce your app's initial bundle size and improve load performance.

Still have questions?
Find answers in the Dynatrace Community