Over the last several months, as part of the Gatsby Concierge program, we’ve been working with a number of teams to improve the performance of their website. A lot of developers wonder why their app bundles grow so large. They can see that it’s big but they can’t figure out why. One common reason we have seen is using a flexible layout component without a more fine-grained approach to code-splitting.
What is a flexible layout component?
When you build a page with a headless CMS, parts of your site may have a structured layout that is defined in code by components and can only be changed with a code change. Other pages, due to business cases or user preferences, need to have flexible layouts where a content editor can mix and reorder components in the CMS, and have that layout reflected in the code.
By and large, both of these scenarios work great in Gatsby. The second, flexible, layout may have unintended consequences if it:
- uses a large amount of components or
- has rich components with heavy dependencies
Let’s look at several patterns that these pages may follow. Assume that all have the following imports at the top:
The most common pattern is a switch case or series of if statements checking the component’s type, rendering the correct one and passing in its props.
Another example is when using MDX or a similar component provider with a map of components to MDX elements:
Lastly, even in small scale situations where you have a form of conditional rendering, the loadable-components technique is beneficial.
So what’s the problem with the patterns above? A content editor can build a page with a Heading, Text or a Carousel. Technically, the page will display as expected, but imagine a page where the editor only chooses a Heading and Text. In that case, the Carousel and its dependencies will still be on the page! This is because Gatsby’s default code splitting configuration we mentioned at the beginning splits bundles by page-level. That means any import in a page template will end up in its bundle whether it is actually rendered, used, or not.
This table is a useful primer for Gatsby’s code-splitting configuration.
|App||entry point for site, Gatsby framework, app context||app-[hash].js|
|Webpack Runtime||Webpack code that coordinates bundle interaction||webpack-runtime-[hash].js|
|Framework||for React / React-DOM||framework-[hash].js|
|Commons||libraries used on every Gatsby page||commons-[hash].js|
|Component*||for each page component and the components it uses||component–[file-path]-[hash].js|
|Shared*||libraries used by more than one page||[hash].js|
|Library*||libraries larger than 160 Kb||[hash].js|
|Dynamic Imports*||JS imported using `import()` syntax||[bundle #]-[hash].js|
* There can be multiple bundles of this kind
Luckily, there is a straightforward path to only bundling the components that are actually used on the page. To do that, we reach for loadable-components. This library is React’s recommended way to code-split components that need server-side rendering, which we do for Gatsby sites. Even loadable-components needs some configuration in order to work with SSR though, so we will use a plugin that adjusts the webpack configuration for us with Gatsby.
npm install @loadable/component gatsby-plugin-loadable-components-ssr
Then, add the plugin to your gatsby-config.js
Now, back in our original example, it’s time to use the library on a component. Import the library:
Then, update the imports to follow the loadable API. The `loadable` import is a function that takes a function that returns an import.
Note that non-default exports would be imported like this, using the resolveComponent option:
What does this mean for our bundles now? We can visualize it like this:
Before, all pages would have gotten the bundle below, whether the Carousel was chosen by a content editor or not.
After, with loadable, on a page with the Carousel you would see this:
On a page without the Carousel, you would see only the Header and Text so the user doesn’t get the unnecessary Carousel at all!
An important note that you may have noticed is that just code-splitting is great, but we really need to maintain SSR for performance reasons. If you try to use loadable without the plugin, it may seem like it is working because the bundles look correct, but if you lose SSR for client-side rendering, that performance hit will be way worse than any bundle savings can make up for. So be sure to use the plugin!
For implementing this, be sure to look for the flexible patterns mentioned above. Generally it is best to do this pattern either:
- At the page-level
- If a component has an expensive conditional render (e.g. the Carousel component above is not in a page’s, but in a component’s logic)
Thanks for taking the time to read this today! If you still have questions or are interested in getting personalized help to pinpoint performance problems on your site, take advantage of Gatsby Concierge which offers specialized options to supercharge your website performance.