Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow code splitting #1411

Merged
merged 44 commits into from
Feb 17, 2021
Merged

Allow code splitting #1411

merged 44 commits into from
Feb 17, 2021

Conversation

Gnito
Copy link
Contributor

@Gnito Gnito commented Feb 8, 2021

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 on web app and server.

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

With the upcoming update (Sharetribe-scripts v5.0.0), we are changing 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 the code (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, we have used 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 module bundler comes across these loadable objects, they 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 you loaded /about page, you received main.bundle.js & main.bundle.css, which were pretty huge files containing all the code that was needed to create FTW-daily 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.

So, 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 kind of ListingPages for different types of listings makes more sense with code-splitting. Otherwise, new pages would have performance impact on initial page load 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 the new code

With code-splitting, this resource loading setup changes a bit. If you open /about page on top of this branch, you'll notice that there are several JS & CSS files loaded:

  • Main chunk (e.g. main.1df6bb19.chunk.js). It contains 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 you navigate to 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.

How flickering is optimized with preloading

This means that there might be a fast flickering of blank page when navigation happens first time to a new page. To remedy that situation, we have forced the page-chunk to be preloaded when 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.

Changes to route configuration

To make this preloading possible, we refactored routes in routeConfiguration.js file a bit: Loadable component is directly set to "component" conf:

    // const AuthenticationPage = loadable(() => import(/* webpackChunkName: "AuthenticationPage" */ './containers/AuthenticationPage/AuthenticationPage'));
    {
      path: '/signup',
      name: 'SignupPage',
-      component: props => <AuthenticationPage {...props} tab="signup" />,
+      component: AuthenticationPage,
+      extraProps: { tab: 'signup' },
    },

Change to loadData function setup

Another change is how loadData & setInitialValues static functions work. Since page component is now a Promise (since page chunk is loaded asynchronously), we had to access those functions differently. To minimize the needed changes, we ended up collecting those Redux functions through duck-files - similarly than how src/containers/reducers.js does. So, all the loadData and setInitialValues functions are collected in src/containers/pageDataLoadingAPI.js. For routing, this means changes like:

    {
      path: '/l/:slug/:id',
      name: 'ListingPage',
-      component: props => <ListingPage {...props} />,
-      loadData: ListingPage.loadData,
+      component: ListingPage,
+      loadData: pageDataLoadingAPI.ListingPage.loadData,
    },

This also forced us to standardize the location of loadData functions. Some of the pages have had those is page's main file and not in the duck.js file - most notably SearchPage. Now all the loadData functions are defined in page's own modular Redux file (i.e. SomePageComponent.duck.js).

CSS chunk changes

To ensure that every page-level CSS chunks have custom media queries included, we have separated those breakpoints to src/styles/customMediaQueries.css file and imported it to every page's stylesheet.

Server-side rendering (SSR)

Because pageDataLoadingAPI.js allowed us to keep the routeConfiguration.js file pretty close to original setup, the data loading for SSR works pretty much the same as before:

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

However, Loadable Components setup did change the way we import the app-related code:

  • build directory contains now node subdirectory.
  • Both builds have also 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 entrypoint from node build and passes relevant info to dataLoader.loadData() and rendered.render() calls.
  • Web extractor is used to collect those chunks that the current page-load needs.
    In practice, renderApp function wraps the app with webExtractor.collectChunks and with that 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.

@Gnito Gnito force-pushed the code-splitting-dataloader-api branch 3 times, most recently from a7a1fc5 to d8a8b12 Compare February 10, 2021 13:07
@Gnito Gnito temporarily deployed to sharetribe-starter-app February 10, 2021 13:10 Inactive
@Gnito Gnito force-pushed the code-splitting-dataloader-api branch from d8a8b12 to ab9eda8 Compare February 10, 2021 13:31
@Gnito Gnito temporarily deployed to sharetribe-starter-app February 10, 2021 13:35 Inactive
@Gnito Gnito force-pushed the code-splitting-dataloader-api branch from ab9eda8 to 6ab8f06 Compare February 15, 2021 14:58
@Gnito Gnito temporarily deployed to sharetribe-starter-app February 15, 2021 15:00 Inactive
@Gnito Gnito force-pushed the code-splitting-dataloader-api branch 2 times, most recently from 6964a7b to 77e63ef Compare February 16, 2021 12:06
@Gnito Gnito changed the title WiP: figure out how to allow code splitting Allow code splitting Feb 16, 2021
server/renderer.js Outdated Show resolved Hide resolved
src/containers/pageDataLoadingAPI.js Outdated Show resolved Hide resolved
@Gnito Gnito force-pushed the code-splitting-dataloader-api branch from 77e63ef to caab97f Compare February 16, 2021 16:36
@Gnito Gnito removed the in progress label Feb 17, 2021
Gnito and others added 24 commits February 17, 2021 11:53
Loadable Components become unreadable
Now that pages are imported asynchronously, we cannot fully render
them without changing the test setup. If the server rendering needs
specific tests, then it should probably be based on the server build
and the proper Express server.
@Gnito Gnito force-pushed the code-splitting-dataloader-api branch from caab97f to 899ccf7 Compare February 17, 2021 10:02
@Gnito Gnito merged commit 09a1815 into master Feb 17, 2021
@Gnito Gnito deleted the code-splitting-dataloader-api branch February 17, 2021 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants