Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.

Code splitting with typescript, webpack and React lazy

March 15, 2019

I am writing this because everytime I have to set up code splitting with typescript, webpack and React, I forget how I did it last time and I cannot find any good documentation about this online.

There are many, many, many blog posts about why code splitting is a good idea. I’ll link to the webpack post here if you don’t already know why.

React.lazy makes this super easy from a javascript perspective but there is still a bit of work to do in typescript.

Now you might not need to do this if you are using babel’s typescript support but I’m not and this is how I’ve done it without.

tsconfig.json

First of all, ensure you have the following tsconfig.json compiler settings set:

"compilerOptions": {
  "esModuleInterop": true,
  "lib": ["es5", "es2015", "es2016", "dom"],
  "module": "esnext",
  "moduleResolution": "node",

You don’t strictly have to set esModuleInterop but I always have this set as it stops me having to use the confusing import * as blah from 'blah'.

There is a great post about this here.

React.lazy works best with default exports and even though I would rather never use default exports it is best not to swim against the tide.

You need to ensure that es2016 is added to the lib attribute on line 3 to ensure things like native promises are included in the compilation.

The module attribute is set to esnext as dynamic imports are still at the proposal stage and will not be included with the usual commonjs setting.

You also need to set moduleResolution to node. Commonjs modules turn this on by default, but you have to opt in to node style resolution with other module formats.

Magical webpack comments

I am manually having to add the magical webpack comments webpackChunkName and webpackPrefetch to any dynamic import. I would love to have a better solution for this but below is an example with the comments added:

const TreeContainer = React.lazy(() => import(/*
  webpackChunkName: "tree-container",
  webpackPrefetch: true
*/ '../TreeContainer'))

The webpackChunkName is pretty self explanatory as the outputed chunk file name, e.g. dist/static/js/tree-container.5a8e5b80.chunk.js.

webpackPrefetch will result in the following link tag being appended to the head element:

<link rel="prefetch" as="script" href="/static/js/tree-container.chunk.js">

Link prefetching is a browser mechanism which utilises browser idle time to download or prefetch documents. A web page provides a set of prefetching hints to the browser, and after the browser is finished loading the page, it begins silently prefetching documents and stores them in the cache.

webpack.config

Lastly… or firstly, it does not really matter, you will need to ensure the output section of your webpack.config has these enteries:

  output: {
    path: isStaticBuild ? paths.appBuild : paths.appBuildPublic,
    publicPath: isDevelopment ? `${protocol}://${host}:${devServerPort}/` : '/',
    pathinfo: isDevelopment,
    filename: isDevelopment ? 'static/js/[name].js' : 'static/js/[name].[chunkhash:8].js',
    chunkFilename: isDevelopment ? 'static/js/[name].chunk.js' : 'static/js/[name].[chunkhash:8].chunk.js',
  }

The important lines are lines 5 and 6 which will create the correct chunk file names.

Output

With the following enabled, I can now see my components being chunked in dev mode:

network tab

A build creates the following ssets:

build file sizes

Integration

I don’t think my use case for code splitting could be any more real world than is imaginable. I want to render react components into an existing legacy .net website that still makes heavy use of jquery.

I can’t use the html-webpack-plugin directly as I will be adding the script and link tags to a legacy .NET MVC application with it’s own markup but I can at least use it to generate the correct script tags and link tags.

I took inspiration from the latest create-react-app and used the react-dev-utils inlineChunkHtmlPlugin to create an index.html that I can use to extract the correct script tags and javascript to add to the legacy site.

I added these lines to my all purpose webpack.config.js:

plugins: [
  (isStaticBuild && templateExists) &&
    new HtmlWebpackPlugin({
      inject: true,
      template,
      minify: isProduction && {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    }),
  isProduction && isStaticBuild && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),

Line 19 adds the InlineChunkHtmlPlugin to the plugins section of the webpack config with the HtmlWebPackPlugin that is created in line 3.

I can then hijack the correct script and link tags from the generated index.html file.


Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.