Optimal strategy of code segmentation in SSR scenario
The best SSR framework in the Serverless scenario
What is code splitting
Separate chunks are packaged according to the granularity of page components (or individual components) so that only the home page components are loaded when we visit the home page and the details page components are loaded when we switch routes.
preface
This article is about the isomorphic scene SSR (React SSR, Vue SSR) technology stack, how to carry out route segmentation to load on demand implementation scheme. At the same time, it is also the scheme adopted by the best SSR framework in the Serverless scenario. They are deployed in the online experience addresses of Ali Cloud and Tencent Cloud. Why write this article, because most of the solutions on the market are wrong or complicated. Even the standard practices of official recommended libraries like React-loadable or @loadable/ Component seem to me extremely complicated and unnecessary. Although it is very easy for a pure client SPA application to do route segmentation, especially in vue-Router, you don’t even need to import any third-party libraries and just change the way components are imported to load on demand. But the problem gets more complicated when it comes to server-side rendering. The following should be as much knowledge as you can get
- Have configured the Webpack configuration of code packaging in SSR scenario and know the difference between client-side code packaging and server-side code packaging.
How to quickly create projects in a local experience
$NPM init ssr-app my-ssr-project $NPM init ssr-app my-ssr-project $NPM init ssr-app my-ssr-projectCopy the code
Official Recommendation Strategy
If you’ve ever used an SSR technology stack. Then you should know that our front-end component code will be packaged as a client bundle and a server bundle, respectively
- Server bundle: Runs on the server side, using apis provided by the framework such as renderToNodeStream provided by React-DOM to compile components into HTML strings or streams that are returned to the browser. Because node_modules exists in the server runtime environment, the externals option is usually enabled during packaging to exclude third-party dependencies. The runtime dynamically loads the server bundle from node_modules, making the server bundle small in size.
- Client bundle: Running on the client side, the DOM generated by the server is reused to activate the DOM and add event responses to it. The difference from traditional SPA-packaged bundles is that the DOM is reused or the DOM is recreated on the client side
This is the official react-Loadable solution for SSR scenarios. Here is only part of the screenshot. It’s probably tens of thousands of words too small. And the configuration is very complex, not only to introduce the official high-level components, but also supporting the corresponding Babel plug-in, Webpack plug-in used together. I briefly summarize the purpose of the official strategy: 1. Service end bundle chunking 2. Client end bundle chunking 3. As well as static resource injection on the page, the first requirement to fulfill these requirements is that the server bundle also needs to land on disk during local development. Solutions such as Easy-Webpack cannot be loadable when the server bundle has memory that can be read by memory-FS while being developed locally.
Better strategy
But on second thought, do we really need to follow the official line? As mentioned above, the server bundle is small because the externals option is enabled. The performance benefit of dynamically loading it into chunks is actually very low. Our main goal is to chunk the client bundle, because the client network environment is uncertain and harsh.
Decide whether to slice or not depending on the environment
Our first improvement eliminates the need to block the server bundle. You just need to chunk the client bundle. We use webPack definePlugin to inject a variable to identify whether we are currently client-side or server-side packaged,
config.plugin('define').use(webpack.DefinePlugin, [{
__isBrowser__: false
}])
Copy the code
Because our framework uses reductive routing. ChunkName can be determined by the folder name when generating the routing table. ChunkName can also be defined by a simple convention for declarative routing: github.com/ykfe/ssr/bl…
routes = routes.replace(/"component":("(.+?) ")/g.(global, m1, m2) = > {
constcurrentWebpackChunkName = re.exec(routes)! [2]
return `"component": __isBrowser__ ? require('react-loadable')({
loader: () => import(/* webpackChunkName: "${currentWebpackChunkName}"* /"${m2.replace(/\^/g.'"')}'),
loading: function Loading () {
return require('React').createElement('div')
}
}) : require('${m2.replace(/\^/g.'"')}').default`
})
Copy the code
The following routing table is generated
performnpm run build
The following resources are generated after the command is built.You can see that there is still only one server bundlePage.server.js
The size is 15.8 KB, and the client bundle is splitpage.chunk
index.chunk
detail.chunk
Index.chunk and detail.chunk are dynamically loaded at run time by Page. chunk. The CSS file is also chunked
Preload the home page route
Because loadable and other schemes need to load components asynchronously. But in the SSR scenario, we need the Hydrate API to determine if the DOM generated by the server matches the DOM structure generated by the first render on the client. An error is reported if there is no match. In this case, if we use the loadable client component, the DOM structure will need to match after the asynchronous loading is complete. So at this point we need to preload the components needed for the home page. I wrote a preLoad method to do this
import { FeRouteItem } from 'ssr-types'
const pathToRegexp = require('path-to-regexp')
const preloadComponent = async (Routes: FeRouteItem[]) => {
const pathName = location.pathname
for (const route of Routes) {
const { component, path } = route
let activeComponent = component
if (activeComponent.preload && pathToRegexp(path).test(pathName)) {
activeComponent = (await activeComponent.preload()).default
}
route.component = activeComponent
}
return Routes
}
export { preloadComponent }
Copy the code
According to the current pathname to match the routing table to find the first screen required components to preload in advance. This way we don’t report dom mismatches at runtime.
Preloads the CSS file
One problem with loadable is that asynchronous component CSS files are also loaded dynamically in JS. This will lead to a very bad user experience of the flash screen phenomenon. The official policy still requires user self-injection through Webpack to collect dependencies between modules. Since we use an abbreviated route, we can easily know the name of the CSS chunk corresponding to the first screen to inject in the page
if (dynamic) {
cssOrder.push(`${routeItem.webpackChunkName}.css`)}Copy the code
At this point, our code segmentation scheme in an SSR scenario is completed. There is absolutely no need to do any Webpack retrofit and advanced component packages. Much easier to use than the official scheme.