Client-side bundle optimization
To reduce client-side bundle, there are multiple approach that you can take:
Code Splitting
Instead of loading all assets upfront, you should only load asset what you need for initial render. How do you determine initial render? You can use simple heuristic like this:
Is this component rendered behind conditional branches?
If yes, you might want to replace component import with dynamic import via next/dynamic. Supposed you have component like this
import React from 'react';
import Old from './Old';
import New from './New';
function Component() {
if (someting) {
return <New />;
}
return <Old />;
}
You can rewrite that to this:
import React from 'react';
import dynamic from 'next/dynamic';
const Old = dynamic(() => import('./Old'));
const New = dynamic(() => import('./New'));
function Component() {
if (someting) {
return <New />;
}
return <Old />;
}
Using next/dynamic means we still get the benefit of server-rendered pages. This is a simple change that you can make that may result in big perf improvement.
Keep in mind that not all conditional branches are the same, code splitting is about balancing perceived performance with complexity that arises from loading states.
If you're using resource data, don't forget to render ResourceBoundary!
Is this component visible above the fold?
For components that is not visible above the fold in initial render, like Footer, you can use progressive to enable progressive hydration for this component. What this means is that JS needed for this component will only loaded if the component is visible in the viewport:
import React from 'react';
import { progressive } from '@traveloka/core/experimental';
import AboveTheFold from '../../fold/above';
const BelowTheFold = progressive(() => import('../../fold/below'), {
id: 'some-id',
});
function Page() {
return (
<>
<AboveTheFold />
<BelowTheFold />
</>
);
}
If your page has SEO requirement where you need to render complex components, this should be your preferred method. Server-rendered output still works the same, the only change in behavior is when loading assets in browser. See this video from Google I/O '19 for in depth explanation of different rendering methods for web application.
Does this component need to be hydrated?
Not all components are created equal and due to our unique requirement for SEO, some components are inherently static and doesn't benefit much from client-side hydration. If you're sure that your component is static you can use staticSubtree to mark it as such and prevent hydration, thus reducing your client-side JS bundle. Similar with progressive, your component will server-side rendered normally.
import React from 'react';
import { staticSubtree } from '@traveloka/core/experimental';
import DynamicComponent from '../../dynamic';
const StaticComponent = staticSubtree(() => import('../../static'), {
id: 'some-id',
fallback: <Spinner />,
});
function Page() {
return (
<>
<DynamicComponent />
<StaticComponent />
</>
);
}
Note that because this component is not hydrated, you should expect no interactivity at all in browser. Despite that limitation, this approach still works on client-side navigation because we still load the asset when rendering in client-side navigation context, but you need to make sure you don't rely on browser API or react lifecycle and effect hooks.
Both progressive and staticSubtree accept fallback field that you can use to render your component while it fetches assets, especially if you use one of the resource API (useResource or use*Resource). Using both, you can combine to create partially-interactive server-rendered application that produces less JS.
import React from 'react';
import { progressive, staticSubtree } from '@traveloka/core/experimental';
const Dynamic = progressive(() => import('../../dynamic'), {
id: 'dynamic',
fallback: <Spinner />,
});
const Static = staticSubtree(() => import('../../static'), {
id: 'some-id',
fallback: <Spinner />,
});
function Page() {
return (
<>
<Dynamic />
<Static />
</>
);
}
Client-side specific bundle
We have configured our webpack config in a way that you can create import that resolves to two different files for server bundle and client bundle. This create another layer indirection in your codebase, so make sure you get clear benefit when doing this.
To create client-side specific bundle, create another file with .client extension prefix. For example if you have FlightSearch.tsx file, you can create FlightSearch.client.tsx. When bundling for client-side, our framework build system will prefer files with .client extension.
+ packages/
+- flight/
+- search/
|- FlightSearch.tsx
+- FlightSearch.client.tsx
Beside performance benefit, using this technique allow you to have fastly different logic between server-rendered component and client-side rendered component. I suggest you use strict typing, pure function, and explicit return (instead of mutable data and side effect) so you can be sure that given the same input, it will produce the exact same output.
If you create a component using this method, make sure you're aware of React hydration pitfall.