Figuring out how Next.js handles code splitting and routing so that we can decide if it fits our needs or if we can take inspiration from it for our own internal solution.
There is no special documentation about Next.js SSR, since it's supported out of the box and handled transparently.
Next.js allows to customize the webpack config and provides an isServer
boolean:
// next.config.js
module.exports = {
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
// Perform customizations to webpack config
return config
},
webpackDevMiddleware: config => {
// Perform customizations to webpack dev middleware config
return config
}
}
It also allows to separate server-only runtime config:
// next.config.js
module.exports = {
serverRuntimeConfig: {
// Will only be available on the server side
},
publicRuntimeConfig: {
// Will be available on both server and client
}
}
Some sneak peaks on Next's internal server-side rendering and client-side rendering:
// packages/next-server/server/next-server.js
// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
const { serverRuntimeConfig = {}, publicRuntimeConfig, assetPrefix, generateEtags } = this.nextConfig;
// Abstracts route creation in a helper class
const routes = this.generateRoutes()
this.router = new Router(routes)
this.setAssetPrefix(assetPrefix)
// ...
async run (req, res, parsedUrl) {
try {
const fn = this.router.match(req, res, parsedUrl)
if (fn) {
await fn()
return
}
} catch (err) {
if (err.code === 'DECODE_FAILED') {
res.statusCode = 400
return this.renderError(null, req, res, '/_error', {})
}
throw err
}
if (req.method === 'GET' || req.method === 'HEAD') {
await this.render404(req, res, parsedUrl)
} else {
res.statusCode = 501
res.end('Not Implemented')
}
}
// Uses `react-dom/server` for render
const render = staticMarkup ? renderToStaticMarkup : renderToString
// Dumps data into a `__NEXT_DATA__` document prop
const doc = <Document {...{
__NEXT_DATA__: {
props, // The result of getInitialProps
page, // The rendered page
query, // querystring parsed / passed by the user
// ...
// packages/next/client/index.js
// Uses 'react-dom' for render
let isInitialRender = true
function renderReactElement (reactEl, domEl) {
// The check for `.hydrate` is there to support React alternatives like preact
if (isInitialRender && typeof ReactDOM.hydrate === 'function') {
ReactDOM.hydrate(reactEl, domEl)
isInitialRender = false
} else {
ReactDOM.render(reactEl, domEl)
}
}
// Uses SSR's `__NEXT_DATA__` and moves it to `window`
const data = JSON.parse(document.getElementById('__NEXT_DATA__').textContent)
window.__NEXT_DATA__ = data
// Initialize next/config with the environment configuration
envConfig.setConfig({
serverRuntimeConfig: {},
publicRuntimeConfig: data.runtimeConfig
})
// Uses `<ErrorBoundary />` in production,
// and react-error-overlay in development.
if (process.env.NODE_ENV === 'development') {
renderReactElement((
<App {...appProps} />
), appContainer)
} else {
// ...
renderReactElement((
<ErrorBoundary onError={onError}>
<App {...appProps} />
</ErrorBoundary>
), appContainer)
}
We are already doing SSR with React Router (documentation).
https://nextjs.org/docs#automatic-code-splitting
Next.js does code splitting at many levels: by chunks, something called runtime and by page. It also creates a bundle for React.
_next/static
|- chunks
|- {someNumber}.{hash}.js
|- commons.{hash}.js
|- runtime
|- main-{hash}.js
|- webpack-{hash}.js
|- {hash}/pages
|- _app.js
|- _error.js
|- about.js
|- index.js
Page bundles are loaded dynamically. To decide if a module is included in the main bundle or in the page bundle, Next checks if the module is used at-least in half of your pages. If it is, then it moves into the main JavaScript bundle. If not, that module stays inside the page's bundle.
Some sneak peaks on Next's internal Webpack config:
// Prevents code splitting for SSR code
if (isServer) {
return {
splitChunks: false,
minimize: false
}
}
// The default config
const config: any = {
runtimeChunk: {
name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK
},
splitChunks: {
cacheGroups: {
default: false,
vendors: false
}
}
}
// Only enabled in production
// This logic will create a commons bundle
// with modules that are used in 50% of all pages
config.splitChunks.chunks = 'all'
config.splitChunks.cacheGroups.commons = {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2
}
config.splitChunks.cacheGroups.react = {
name: 'commons',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/
}
let webpackConfig = {
// Uses `cheap-module-source-map` for source maps
devtool: dev ? 'cheap-module-source-map' : false,
// ...
ouptut: {
// ...
// Naming convention
chunkFilename: isServer ? `${dev ? '[name]' : '[name].[contenthash]'}.js` : `static/chunks/${dev ? '[name]' : '[name].[contenthash]'}.js`,
// ...
},
// ...
plugins: [
// Precompile react / react-dom for development, speeding up webpack
dev && !isServer && new AutoDllPlugin({ ... }),
// ...
],
}
https://reacttraining.com/react-router/web/guides/code-splitting
Webpack 4 has built-in support for dynamic imports; if you are using Babel you need to use the @babel/plugin-syntax-dynamic-import
plugin. The plugin simply allows Babel to parse dynamic imports so Webpack can bundle them as a code split.
import React from 'react'
import dynamic from 'next/dynamic'
const Component = dynamic(import('component-path'), {
loading: () => <div>Loading...</div>
})
<Component />
import React, { lazy } from 'react'
const Component = lazy(() => import('component-path'))
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
import Loadable from 'react-loadable';
const Component = Loadable({
loader: () => import('component-path'),
loading: () => <div>Loading...</div>
});
<Component />;
React is still not ready for SSR.
Next.js is prepared for SSR of dynamic components. It also provides configuration:
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(() => import('../components/hello3'), {
ssr: false
})
Alternatively, react-loadable
also supports SSR. All you should need to do is include babel-plugin-import-inspector
in .babelrc
.
{
"presets": ["@babel/react"],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
[
"import-inspector",
{
"serverSideRequirePath": true
}
]
]
}
Next.js default file-system routing can be disabled (documentation).
// next.config.js
module.exports = {
useFileSystemPublicRoutes: false
}
Of interest:
- Next.js has a
router.prefetch
to "imperatively" prefetch pages.
Both work similarly.
import { Link } from 'react-router-dom';
<Link to="/about">About</Link>
import Link from 'next/link';
<Link href="/about"><a>About</a></Link>
Of interest:
- React Router's has prop
innerRef: function
to link to anchors. - Next.js has prop
prefetch
to refetch the linked pages in the background.
- Abstracts many interesting things like SSR, dynamic imports, routing.
- A small Next main bundle is around 65kb gzipped.
- Some interesting things: supports wasm and TypeScript.
- Some of the most attractive offerings are not interesting to us: file-system routing (unless we change our file structure), static HTML export, production deployment, multi zones deployment, styled-jsx, etc.
- It's very opinionated (for example on routing or CSS), which means we have to change a lot of code if we want to take advantage of it.
- Customizing the routes, error pages, document DOM, etc. is possible, but result in a complex config file and a similar amount of code as a custom solution but with Next's file structure.