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)
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:
- server/dataLoader.js initializes store
- It also figures out which route is used and fetches route configuration for it
- If the configuration contains a loadData function, the call is dispatched
- 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.