Large bundle sizes and slow startup is a common problem faced by single-page applications (SPAs), since they typically download all the JavaScript required for every single page of the application right at the start, before rendering a single pixel. A simple way to solve this problem is to use code-splitting i.e. breaking down the application’s JavaScript into small, modular bundles called chunks, which can be loaded on-demand when a particular feature is accessed. The goal is to keep individual chunks under 100–150 KB, so that the application becomes interactive within 4–5 seconds, even on poor networks. Component-based code splitting The open source library react-loadable provides a React-friendly API for code splitting, and lets you add breakpoints with just a few lines of code. If you're using create-react-app, Webpack automatically takes care of splitting the bundle and loading chunks on demand under the hood. Here’s how it works: suppose we want to load and render the component SettingsPage on demand, when the user clicks on a particular or navigates to a particular route. All we need to is wrap it using react-loadable as follows: import Loadable from "react-loadable"; import Loading from "./Loading"; const AsyncSettingsPage = Loadable({ loader: () => import("./SettingsPage"), loading: Loading }); export { AsyncSettingsPage }; Now we can use AsyncSettingsPage just like a normal React component. The module SettingsPage.js and its dependencies are no longer a part of the main JavaScript bundle and are loaded asynchronously when AsyncSettingsPage is rendered for the first time. While the chunk is loading, the component Loading is rendered in its place. Here’s a sample implementation of Loading: import React from "react"; const Loading = props => { if (props.error) { return <div>Error!</div>; } else { return <div>Loading...</div>; } }; export default Loading; The prop error is set to a non-null value if the chunk fails to load. Chunking multiple components together There are some cases where simple component-based splitting may not be enough. For instance, you may have a set of components that are almost always used together in a several different features. In such a case, it makes sense to have a single chunk which contains the entire set of related components. Here’s how we might normally export a set of related components: import ItemListHeader from "./ItemListHeader"; import ItemListFilters from "./ItemListFilters"; import ItemListTable from "./ItemListTable"; export { ItemListHeader, ItemListFilters, ItemListTable }; Assuming the above code is in the file item-list/index.js, we can create another file item-list/async.js with the following contents: import React from "react"; import Loadable from "react-loadable"; import Loading from "./Loading"; const AsyncItemListHeader = Loadable({ loader: () => import("./index").then(m => m.ItemListHeader), loading: Loading }); const AsyncItemListFilters = Loadable({ loader: () => import("./index").then(m => m.ItemListFilters), loading: Loading }); const AsyncItemListTable = Loadable({ loader: () => import("./index").then(m => m.ItemListTable), loading: Loading }); export { AsyncItemListHeader, AsyncItemListFilters, AsyncItemListTable, }; The key change here is in the dynamic import: instead of importing a single component, we are importing all of index.js and extracting the required component in the promise callback. Chunk naming and optimization When we build application for production after implementing code splitting, we get many chunks of Javascript that look like this: File sizes after gzip: 396.71 KB build/static/js/main.3a8842c0.js 178.51 KB build/static/css/main.e32b4522.css 68.31 KB build/static/js/6.af93367f.chunk.js 44.34 KB build/static/js/2.6a7f1417.chunk.js 23.61 KB build/static/js/1.bdfdcd83.chunk.js 22.24 KB build/static/js/3.d9e4ee99.chunk.js 19.29 KB build/static/js/4.a66b3cdb.chunk.js 17.1 KB build/static/js/5.f1ce26f7.chunk.js 7.63 KB build/static/js/8.2e807534.chunk.js 6.71 KB build/static/js/9.409015da.chunk.js 5.09 KB build/static/js/7.1b95d8e8.chunk.js 1.71 KB build/static/js/0.6bea2af7.chunk.js 1 KB build/static/js/10.ce9f2434.chunk.js After looking at this output, we might want to remove some of the last few chunks since they’re really small. But we don’t know which split is causing which chunk to be created. This is where chunk naming can be helpful. We can use a magic comment inside the import that tells Webpack to use the given name for a specific chunk: const AsyncSettingsPage = Loadable({ loader: () => import("./SettingsPage" /* webpackChunkName: "settings" */), loading: Loading }); Once all the chunks are named, we can identify the splits that lead to smaller chunks: File sizes after gzip: 312.09 KB build/static/js/main.491eaaf4.js 181 KB build/static/css/main.ac06cedb.css 68.88 KB build/static/js/settings.1525d075.chunk.js 45.08 KB build/static/js/alerts.0f5ad4d6.chunk.js 23.62 KB build/static/js/profile.199c7f90.chunk.js 22.24 KB build/static/js/history.07ccea31.chunk.js 19.3 KB build/static/js/actions.903378a5.chunk.js 8.87 KB build/static/js/events.f540de3a.chunk.js 7.62 KB build/static/js/colors.89aa1e6f.chunk.js 6.7 KB build/static/js/posts.929f04fc.chunk.js 5.1 KB build/static/js/post-details.6c133f77.chunk.js 1.71 KB build/static/js/friend-list.be516e45.chunk.js 1.01 KB build/static/js/edit-avatar.33a4ff21.chunk.js At this point, we can choose to remove or combine some of the smaller chunks (< 20–30 KB in size), since the overhead of loading a 5 KB chunk might be higher than combining it with one of the larger chunks. Play around with different splits and see what works best for you. Analyzing the Bundle Size Source map explorer analyzes JavaScript bundles using the source maps. This helps you understand where code bloat is coming from. To add Source map explorer to a Create React App project, run the following command: npm install --save source-map-explorer Then in package.json, add the following line to scripts: "scripts": { "analyze": "source-map-explorer build/static/js/main.*", Then to analyze the bundle run the production build then run the analyze script. npm run build npm run analyze Source map explorer Look for the largest contributors to the bundle size as possible candidates for code-splitting. Also consider removing or pruning large dependencies from node_modules. Summary Here are steps for achieving effective code splitting in React applications: Use react-loadable to achieve component-based code splitting and load Javascript bundles for different parts of the application on demand.Chunk multiple components that are frequently used together into a single file using the import("./index").then trick.Name your chunk using the magic comment /* webpackChunkName: xxx */ and optimize bundle sizes so that they are neither too small nor too large.Use source-map-explorer to identify possible candidates for code splitting. I’ve skipped over many details to keep this article short and focus on the practical aspects of code splitting. Following are some good places to learn more about the topic: Dynamic import: https://developers.google.com/web/updates/2017/11/dynamic-importReact official docs: https://reactjs.org/docs/code-splitting.htmlreact-loadable: https://github.com/jamiebuilds/react-loadableChunk naming in Webpack: https://github.com/webpack/webpack/tree/master/examples/code-splitting-specify-chunk-name Large bundle sizes and slow startup is a common problem faced by single-page applications (SPAs), since they typically download all the JavaScript required for every single page of the application right at the start, before rendering a single pixel. A simple way to solve this problem is to use code-splitting i.e. breaking down the application’s JavaScript into small, modular bundles called chunks , which can be loaded on-demand when a particular feature is accessed. The goal is to keep individual chunks under 100–150 KB, so that the application becomes interactive within 4–5 seconds, even on poor networks. chunks Component-based code splitting The open source library react-loadable provides a React-friendly API for code splitting, and lets you add breakpoints with just a few lines of code. If you're using create-react-app , Webpack automatically takes care of splitting the bundle and loading chunks on demand under the hood. react-loadable react-loadable react-loadable create-react-app create-react-app create-react-app Webpack Here’s how it works: suppose we want to load and render the component SettingsPage on demand, when the user clicks on a particular or navigates to a particular route. All we need to is wrap it using react-loadable as follows: SettingsPage SettingsPage react-loadable react-loadable import Loadable from "react-loadable"; import Loading from "./Loading"; const AsyncSettingsPage = Loadable({ loader: () => import("./SettingsPage"), loading: Loading }); export { AsyncSettingsPage }; import Loadable from "react-loadable"; import Loading from "./Loading"; const AsyncSettingsPage = Loadable({ loader: () => import("./SettingsPage"), loading: Loading }); export { AsyncSettingsPage }; import Loadable from "react-loadable" ; import Loading from "./Loading" ; const AsyncSettingsPage = Loadable({ loader : () => import ( "./SettingsPage" ), loading : Loading }); export { AsyncSettingsPage }; import Loadable from "react-loadable" ; import Loading from "./Loading" ; const AsyncSettingsPage = Loadable({ loader : () => import ( "./SettingsPage" ), loading : Loading export { AsyncSettingsPage }; Now we can use AsyncSettingsPage just like a normal React component. The module SettingsPage.j s and its dependencies are no longer a part of the main JavaScript bundle and are loaded asynchronously when AsyncSettingsPage is rendered for the first time. AsyncSettingsPage AsyncSettingsPage SettingsPage.j SettingsPage.j AsyncSettingsPage AsyncSettingsPage While the chunk is loading, the component Loadin g is rendered in its place. Here’s a sample implementation of Loading : Loadin Loadin Loading Loading import React from "react"; const Loading = props => { if (props.error) { return <div>Error!</div>; } else { return <div>Loading...</div>; } }; export default Loading; import React from "react"; const Loading = props => { if (props.error) { return <div>Error!</div>; } else { return <div>Loading...</div>; } }; export default Loading; import React from "react" ; const Loading = props => { if (props.error) { return < div > Error! </ div > ; } else { return < div > Loading... </ div > ; } }; export default Loading; import React from "react" ; const Loading = props => { if (props.error) { return < div > Error! </ div > ; } else { return < div > Loading... </ div > ; export default Loading; The prop error is set to a non-null value if the chunk fails to load. error error Chunking multiple components together There are some cases where simple component-based splitting may not be enough. For instance, you may have a set of components that are almost always used together in a several different features. In such a case, it makes sense to have a single chunk which contains the entire set of related components. Here’s how we might normally export a set of related components: import ItemListHeader from "./ItemListHeader"; import ItemListFilters from "./ItemListFilters"; import ItemListTable from "./ItemListTable"; export { ItemListHeader, ItemListFilters, ItemListTable }; import ItemListHeader from "./ItemListHeader"; import ItemListFilters from "./ItemListFilters"; import ItemListTable from "./ItemListTable"; export { ItemListHeader, ItemListFilters, ItemListTable }; import ItemListHeader from "./ItemListHeader" ; import ItemListFilters from "./ItemListFilters" ; import ItemListTable from "./ItemListTable" ; export { ItemListHeader, ItemListFilters, ItemListTable }; import ItemListHeader from "./ItemListHeader" ; import ItemListFilters from "./ItemListFilters" ; import ItemListTable from "./ItemListTable" ; export { Assuming the above code is in the file item-list/index.js , we can create another file item-list/async.js with the following contents: item-list/index.js item-list/index.js item-list/async.js item-list/async.js import React from "react"; import Loadable from "react-loadable"; import Loading from "./Loading"; const AsyncItemListHeader = Loadable({ loader: () => import("./index").then(m => m.ItemListHeader), loading: Loading }); const AsyncItemListFilters = Loadable({ loader: () => import("./index").then(m => m.ItemListFilters), loading: Loading }); const AsyncItemListTable = Loadable({ loader: () => import("./index").then(m => m.ItemListTable), loading: Loading }); export { AsyncItemListHeader, AsyncItemListFilters, AsyncItemListTable, }; import React from "react"; import Loadable from "react-loadable"; import Loading from "./Loading"; const AsyncItemListHeader = Loadable({ loader: () => import("./index").then(m => m.ItemListHeader), loading: Loading }); const AsyncItemListFilters = Loadable({ loader: () => import("./index").then(m => m.ItemListFilters), loading: Loading }); const AsyncItemListTable = Loadable({ loader: () => import("./index").then(m => m.ItemListTable), loading: Loading }); export { AsyncItemListHeader, AsyncItemListFilters, AsyncItemListTable, }; import React from "react" ; import Loadable from "react-loadable" ; import Loading from "./Loading" ; const AsyncItemListHeader = Loadable({ loader : () => import ( "./index" ).then( m => m.ItemListHeader), loading : Loading }); const AsyncItemListFilters = Loadable({ loader : () => import ( "./index" ).then( m => m.ItemListFilters), loading : Loading }); const AsyncItemListTable = Loadable({ loader : () => import ( "./index" ).then( m => m.ItemListTable), loading : Loading }); export { AsyncItemListHeader, AsyncItemListFilters, AsyncItemListTable, }; import React from "react" ; import Loadable from "react-loadable" ; import Loading from "./Loading" ; const AsyncItemListHeader = Loadable({ loader : () => import ( "./index" ).then( m => m.ItemListHeader), loading : Loading const AsyncItemListFilters = Loadable({ loader : () => import ( "./index" ).then( m => m.ItemListFilters), loading : Loading const AsyncItemListTable = Loadable({ loader : () => import ( "./index" ).then( m => m.ItemListTable), loading : Loading export { The key change here is in the dynamic import : instead of importing a single component, we are importing all of index.js and extracting the required component in the promise callback. import import index.js index.js Chunk naming and optimization When we build application for production after implementing code splitting, we get many chunks of Javascript that look like this: File sizes after gzip: 396.71 KB build/static/js/main.3a8842c0.js 178.51 KB build/static/css/main.e32b4522.css 68.31 KB build/static/js/6.af93367f.chunk.js 44.34 KB build/static/js/2.6a7f1417.chunk.js 23.61 KB build/static/js/1.bdfdcd83.chunk.js 22.24 KB build/static/js/3.d9e4ee99.chunk.js 19.29 KB build/static/js/4.a66b3cdb.chunk.js 17.1 KB build/static/js/5.f1ce26f7.chunk.js 7.63 KB build/static/js/8.2e807534.chunk.js 6.71 KB build/static/js/9.409015da.chunk.js 5.09 KB build/static/js/7.1b95d8e8.chunk.js 1.71 KB build/static/js/0.6bea2af7.chunk.js 1 KB build/static/js/10.ce9f2434.chunk.js File sizes after gzip: 396.71 KB build/static/js/main.3a8842c0.js 178.51 KB build/static/css/main.e32b4522.css 68.31 KB build/static/js/6.af93367f.chunk.js 44.34 KB build/static/js/2.6a7f1417.chunk.js 23.61 KB build/static/js/1.bdfdcd83.chunk.js 22.24 KB build/static/js/3.d9e4ee99.chunk.js 19.29 KB build/static/js/4.a66b3cdb.chunk.js 17.1 KB build/static/js/5.f1ce26f7.chunk.js 7.63 KB build/static/js/8.2e807534.chunk.js 6.71 KB build/static/js/9.409015da.chunk.js 5.09 KB build/static/js/7.1b95d8e8.chunk.js 1.71 KB build/static/js/0.6bea2af7.chunk.js 1 KB build/static/js/10.ce9f2434.chunk.js File sizes after gzip: 396.71 KB build/ static /js/main.3a8842c0.js 178.51 KB build/ static /css/main.e32b4522.css 68.31 KB build/ static /js/ 6. af93367f.chunk.js 44.34 KB build/ static /js/ 2. 6a7f1417.chunk.js 23.61 KB build/ static /js/ 1. bdfdcd83.chunk.js 22.24 KB build/ static /js/ 3. d9e4ee99.chunk.js 19.29 KB build/ static /js/ 4. a66b3cdb.chunk.js 17.1 KB build/ static /js/ 5. f1ce26f7.chunk.js 7.63 KB build/ static /js/ 8.2e807534 .chunk.js 6.71 KB build/ static /js/ 9. 409015da.chunk.js 5.09 KB build/ static /js/ 7. 1b95d8e8.chunk.js 1.71 KB build/ static /js/ 0. 6bea2af7.chunk.js 1 KB build/ static /js/ 10. ce9f2434.chunk.js File sizes after gzip: 396.71 KB build/ static /js/main.3a8842c0.js 178.51 KB build/ static /css/main.e32b4522.css 68.31 KB build/ static /js/ 6. af93367f.chunk.js 44.34 KB build/ static /js/ 2. 6a7f1417.chunk.js 23.61 KB build/ static /js/ 1. bdfdcd83.chunk.js 22.24 KB build/ static /js/ 3. d9e4ee99.chunk.js 19.29 KB build/ static /js/ 4. a66b3cdb.chunk.js 17.1 KB build/ static /js/ 5. f1ce26f7.chunk.js 7.63 KB build/ static /js/ 8.2e807534 .chunk.js 6.71 KB build/ static /js/ 9. 409015da.chunk.js 5.09 KB build/ static /js/ 7. 1b95d8e8.chunk.js 1.71 KB build/ static /js/ 0. 6bea2af7.chunk.js 1 KB build/ static /js/ 10. ce9f2434.chunk.js After looking at this output, we might want to remove some of the last few chunks since they’re really small. But we don’t know which split is causing which chunk to be created. This is where chunk naming can be helpful. We can use a magic comment inside the impor t that tells Webpack to use the given name for a specific chunk: magic comment impor impor const AsyncSettingsPage = Loadable({ loader: () => import("./SettingsPage" /* webpackChunkName: "settings" */), loading: Loading }); const AsyncSettingsPage = Loadable({ loader: () => import("./SettingsPage" /* webpackChunkName: "settings" */), loading: Loading }); const AsyncSettingsPage = Loadable({ loader : () => import ( "./SettingsPage" /* webpackChunkName: "settings" */ ), loading : Loading }); const AsyncSettingsPage = Loadable({ loader : () => import ( "./SettingsPage" /* webpackChunkName: "settings" */ ), loading : Loading Once all the chunks are named, we can identify the splits that lead to smaller chunks: File sizes after gzip: 312.09 KB build/static/js/main.491eaaf4.js 181 KB build/static/css/main.ac06cedb.css 68.88 KB build/static/js/settings.1525d075.chunk.js 45.08 KB build/static/js/alerts.0f5ad4d6.chunk.js 23.62 KB build/static/js/profile.199c7f90.chunk.js 22.24 KB build/static/js/history.07ccea31.chunk.js 19.3 KB build/static/js/actions.903378a5.chunk.js 8.87 KB build/static/js/events.f540de3a.chunk.js 7.62 KB build/static/js/colors.89aa1e6f.chunk.js 6.7 KB build/static/js/posts.929f04fc.chunk.js 5.1 KB build/static/js/post-details.6c133f77.chunk.js 1.71 KB build/static/js/friend-list.be516e45.chunk.js 1.01 KB build/static/js/edit-avatar.33a4ff21.chunk.js File sizes after gzip: 312.09 KB build/static/js/main.491eaaf4.js 181 KB build/static/css/main.ac06cedb.css 68.88 KB build/static/js/settings.1525d075.chunk.js 45.08 KB build/static/js/alerts.0f5ad4d6.chunk.js 23.62 KB build/static/js/profile.199c7f90.chunk.js 22.24 KB build/static/js/history.07ccea31.chunk.js 19.3 KB build/static/js/actions.903378a5.chunk.js 8.87 KB build/static/js/events.f540de3a.chunk.js 7.62 KB build/static/js/colors.89aa1e6f.chunk.js 6.7 KB build/static/js/posts.929f04fc.chunk.js 5.1 KB build/static/js/post-details.6c133f77.chunk.js 1.71 KB build/static/js/friend-list.be516e45.chunk.js 1.01 KB build/static/js/edit-avatar.33a4ff21.chunk.js File sizes after gzip: 312.09 KB build/ static /js/main.491eaaf4.js 181 KB build/ static /css/main.ac06cedb.css 68.88 KB build/ static /js/settings.1525d075.chunk.js 45.08 KB build/ static /js/alerts.0f5ad4d6.chunk.js 23.62 KB build/ static /js/profile.199c7f90.chunk.js 22.24 KB build/ static /js/history.07ccea31.chunk.js 19.3 KB build/ static /js/actions.903378a5.chunk.js 8.87 KB build/ static /js/events.f540de3a.chunk.js 7.62 KB build/ static /js/colors.89aa1e6f.chunk.js 6.7 KB build/ static /js/posts.929f04fc.chunk.js 5.1 KB build/ static /js/post-details.6c133f77.chunk.js 1.71 KB build/ static /js/friend-list.be516e45.chunk.js 1.01 KB build/ static /js/edit-avatar.33a4ff21.chunk.js File sizes after gzip: 312.09 KB build/ static /js/main.491eaaf4.js 181 KB build/ static /css/main.ac06cedb.css 68.88 KB build/ static /js/settings.1525d075.chunk.js 45.08 KB build/ static /js/alerts.0f5ad4d6.chunk.js 23.62 KB build/ static /js/profile.199c7f90.chunk.js 22.24 KB build/ static /js/history.07ccea31.chunk.js 19.3 KB build/ static /js/actions.903378a5.chunk.js 8.87 KB build/ static /js/events.f540de3a.chunk.js 7.62 KB build/ static /js/colors.89aa1e6f.chunk.js 6.7 KB build/ static /js/posts.929f04fc.chunk.js 5.1 KB build/ static /js/post-details.6c133f77.chunk.js 1.71 KB build/ static /js/friend-list.be516e45.chunk.js 1.01 KB build/ static /js/edit-avatar.33a4ff21.chunk.js At this point, we can choose to remove or combine some of the smaller chunks (< 20–30 KB in size), since the overhead of loading a 5 KB chunk might be higher than combining it with one of the larger chunks. Play around with different splits and see what works best for you. Analyzing the Bundle Size Source map explorer analyzes JavaScript bundles using the source maps. This helps you understand where code bloat is coming from. To add Source map explorer to a Create React App project, run the following command: Source map explorer npm install --save source-map-explorer npm install --save source-map-explorer npm install --save source-map-explorer npm install --save source-map-explorer Then in package.json , add the following line to scripts : package.json package.json scripts scripts "scripts": { "analyze": "source-map-explorer build/static/js/main.*", "scripts": { "analyze": "source-map-explorer build/static/js/main.*", "scripts" : { "analyze" : "source-map-explorer build/static/js/main.*" , "scripts" : { "analyze" : "source-map-explorer build/static/js/main.*" , Then to analyze the bundle run the production build then run the analyze script. npm run build npm run analyze npm run build npm run analyze npm run build npm run analyze npm run build Source map explorer Source map explorer Look for the largest contributors to the bundle size as possible candidates for code-splitting. Also consider removing or pruning large dependencies from node_modules . node_modules node_modules Summary Here are steps for achieving effective code splitting in React applications: Use react-loadable to achieve component-based code splitting and load Javascript bundles for different parts of the application on demand. Chunk multiple components that are frequently used together into a single file using the import("./index").then trick. Name your chunk using the magic comment /* webpackChunkName: xxx */ and optimize bundle sizes so that they are neither too small nor too large. Use source-map-explorer to identify possible candidates for code splitting. Use react-loadable to achieve component-based code splitting and load Javascript bundles for different parts of the application on demand. react-loadable react-loadable Chunk multiple components that are frequently used together into a single file using the import("./index").then trick. import("./index").then import("./index").then Name your chunk using the magic comment /* webpackChunkName: xxx */ and optimize bundle sizes so that they are neither too small nor too large. /* webpackChunkName: xxx */ /* webpackChunkName: xxx */ Use source-map-explorer to identify possible candidates for code splitting. source-map-explorer source-map-explorer I’ve skipped over many details to keep this article short and focus on the practical aspects of code splitting. Following are some good places to learn more about the topic: Dynamic import: https://developers.google.com/web/updates/2017/11/dynamic-import React official docs: https://reactjs.org/docs/code-splitting.html react-loadable: https://github.com/jamiebuilds/react-loadable Chunk naming in Webpack: https://github.com/webpack/webpack/tree/master/examples/code-splitting-specify-chunk-name Dynamic import : https://developers.google.com/web/updates/2017/11/dynamic-import import import https://developers.google.com/web/updates/2017/11/dynamic-import React official docs: https://reactjs.org/docs/code-splitting.html https://reactjs.org/docs/code-splitting.html react-loadable : https://github.com/jamiebuilds/react-loadable react-loadable react-loadable https://github.com/jamiebuilds/react-loadable Chunk naming in Webpack: https://github.com/webpack/webpack/tree/master/examples/code-splitting-specify-chunk-name https://github.com/webpack/webpack/tree/master/examples/code-splitting-specify-chunk-name