Last updated

How code splitting works in FTW

This article explains how the code splitting setup works in Flex Template for Web (FTW).

Table of Contents

Background info

FTW-daily started using code-splitting from version 8.0.0 and FTW-hourly from 10.0.0.

Previously, sharetribe-scripts created one UMD build that was used on both server and frontend. I.e. all the code used in the app was bundled into a single main.bundle.js file and that was used in the web app and server.

Unfortunately, this has meant that code-splitting was not supported: it didn't work with the UMD build due to an old bug in Webpack.

With sharetribe-scripts version 5.0.0, we changed this behaviour: sharetribe-scripts creates 2 different builds when yarn run build is called. Basically, this means that build-time increases (including yarn run dev-server call).

However, this setup makes code-splitting possible. To make this easier, we have added Loadable Components library to the setup.

What is code splitting

Instead of downloading the entire app before users can use it, code splitting allows us to split code away from one main.bundle.js file into smaller chunks which you can then load on demand. To familiarize yourself with the subject, you could read about code splitting from reactjs.org.

In practice, FTW templates use route-based code splitting: page-level components are now using Loadable Components syntax to create dynamic imports functionality.

const AboutPage = loadable(() =>
  import(
    /* webpackChunkName: "AboutPage" */ './containers/AboutPage/AboutPage'
  )
);

When Webpack comes across these loadable objects, it will create a new JS & CSS chunk files (e.g. AboutPage.dc3102d3.chunk.js). I.e. those code-paths are separated from the main bundle.

Previously, (when code-splitting was not supported), when you loaded /about page, you received main.bundle.js & main.bundle.css. Those files were pretty huge containing all the code that was needed to create a template app and any page inside it. Loading a single file takes time and also browsers had to evaluate the entire JS-file before it was ready to make the app fully functional.

Why you should use it?

The main benefit of code splitting is to reduce the code that is loaded for any single page. That improves the performance, but even more importantly, it makes it possible to add more navigational paths and page-variants to the codebase. For example, adding different kinds of ListingPages for different types of listings makes more sense with code-splitting. Without code splitting, new pages, features, and libraries would have a performance impact on the initial page load of the app and therefore SEO performance would drop too.

Note: currently, most of the code is in shared src/components/ directory and this reduces the benefits that come from code-splitting. In the future, we are probably going to move some components from there to page-specific directories (if they are not truly shared between different pages).

How code splitting works in practice

If you open /about page, you'll notice that there are several JS & CSS files loaded:

  • Main chunk (e.g. main.1df6bb19.chunk.js & main.af610ce4.chunk.css). They contain code that is shared between different pages.
  • Vendor chunk (Currently, it's an unnamed chunk file. e.g. 24.230845cc.chunk.js)
  • Page-specific chunk (e.g. AboutPage.dc3102d3.chunk.js)
  • Runtime chunk (e.g. runtime-main.818a6866.js) This one takes care of loading correct JS & CSS files when you navigate to another page inside the web app. (e.g. it loads LandingPage.6fa732d5.chunk.js && LandingPage.40c0bf91.chunk.css, when a user navigates to the landing page.)

So, there are several chunk files that can be loaded parallel in the first page-load and also page-specific chunks that can be loaded in response to in-app navigation.

Naturally, this means that during in-app navigation there are now more things that the app needs to load: data that the next page needs and code chunk that it needs to render the data. The latter is not needed if the page-specific chunk is already loaded earlier.

Preloading code chunks

Route-based code splitting means that there might be a fast flickering of a blank page when navigation happens for the first time to a new page. To remedy that situation, FTW templates have forced the page-chunks to be preloaded when the mouse is over NamedLink. In addition, Form and Button components can have a property enforcePagePreloadFor="SearchPage". That way the specified chunk is loaded before the user has actually clicked the button or executed form submit.

Route configuration

To make the aforementioned preloading possible, the loadable component is directly set to "component" conf in routeConfigurations.js file:

└── src
    └── routeConfiguration.js
└── src
    └── routing
        └── routeConfiguration.js
    // const AuthenticationPage = loadable(() => import(/* webpackChunkName: "AuthenticationPage" */ './containers/AuthenticationPage/AuthenticationPage'));
    {
      path: '/signup',
      name: 'SignupPage',
      component: AuthenticationPage,
      extraProps: { tab: 'signup' },
    },

Data loading

FTW templates collects loadData and setInitialValues Redux functions from modular Redux file (i.e. files that look like <SomePageComponent>.duck.js). This happens in pageDataLoadingAPI.js:

└── src
    └── containers
        └── pageDataLoadingAPI.js

Then those files can be connected with routing through route configuration.

    // import getPageDataLoadingAPI from './containers/pageDataLoadingAPI';
    // const pageDataLoadingAPI = getPageDataLoadingAPI();
    {
      path: '/l/:slug/:id',
      name: 'ListingPage',
      component: ListingPage,
      loadData: pageDataLoadingAPI.ListingPage.loadData,
    },

CSS chunk changes

To ensure that every page-level CSS chunk has custom media queries included, those breakpoints are included through a separate file (customMediaQueries.css) and it is imported into the main stylesheet of every page.

└── src
    └── styles
        └── customMediaQueries.css

Server-side rendering (SSR)

When FTW templates receive a page-load call on server and the page is a public one ("auth" flag is not set in route configuration), the server will render the page into a string and returns it among HTML code. This process has 4 main steps:

  1. server/dataLoader.js initializes store
  2. It also figures out which route is used and fetches route configuration for it
  3. If the configuration contains a loadData function, the call is dispatched
  4. As a consequence, the store gets populated and it can be used to render the app to a string.

Build directory

Sharetribe-scripts dependency uses Webpack to build the application. It copies the content from public/ directory into the build directory and the Webpack build bundles all the code into files that can be used in production mode.

  • Code for server-side rendering is saved to build/node directory.
  • Code for client-side rendering is saved to build/static directory.
  • Both builds have also a loadable-stats.json file, which basically tells what assets different pages need.
  • server/importer.js exposes two ChunkExtractors - one for web and another for node build.
  • server/index.js requires the entrypoint for the node build, extracts relevant info, and passes them to dataLoader.loadData() and rendered.render() calls.
  • webExtractor (ChunkExtractor for the web build) is used to collect those different code chunks (JS & CSS files) that the current page-load is going to need.

    In practice, renderApp function wraps the app with webExtractor.collectChunks. With that the webExtractor can figure out all the relevant loadable calls that the server uses for the current page and therefore the web-versions of those chunks can be included to rendered pages through <script> tags.