This project can be easily turned into a production-ready boilerplate by following the next steps:
git clone https://github.com/theninthsky/client-side-rendering.git
npm i
npm run boilerplatify
To automatically generate a sitemap, replace the https://example.com
hostname in create-sitemap.js with your own hostname.
To handle dynamic routes, modify the script to first fetch the dynamic routes and then add them to the paths
array.
Here is an example: https://github.com/theninthsky/client-side-rendering/blob/main/scripts/create-sitemap.js
Static assets can be fetched and stored in the CDN during the build process, allowing them to be served very quickly.
The asset generation process should take place in fetch-static.js.
Here is an example that fetches static data a stores it under public/json
: https://github.com/theninthsky/client-side-rendering/blob/main/scripts/fetch-static.js
Abiding by this boilerplate's requirements grants you the following features:
- Extreme vendors splitting for better cache persistence
- Code-splitting
- Preloading of current page scripts
- Prefetching of all scripts for instant navigations
- Preloading of data
- Prerendering for perfect SEO
Tools that optimize for performance usually require the developer to follow a set of rules to "help" them do it.
For example: Next.js and Remix require you to use their file-based routers in order to be able to apply automatic code-splitting and other optimizations, and modern frameworks require the use of signals (which sometimes feel a little unintuitive) to skip full-tree rerenders.
This boilerplate is no exception, thus it requires two things in order to fully apply its optimizations:
- Lazily loading all pages and giving them unique names.
- Maintaining the pages-manifest, a file that specifies the chunk names, paths and data to preload.
Async chunks can be easily named using Webpack's magic comments:
const Home = lazy(() => import(/* webpackChunkName: 'home' */ 'pages/Home'))
const LoremIpsum = lazy(() => import(/* webpackChunkName: 'lorem-ipsum' */ 'pages/LoremIpsum'))
This will create both home.[hash].js
and lorem-ipsum.[hash].js
files (instead of the default, cryptic, [id].[hash].js
files).
To best describe what properties should be in the pages-manifest file, we'll use a TypeScript-like definition:
[
{
chunk: string
path: string
data?:
{
url: string | ((params: { [x: string]: string }) => string)
crossorigin?: string
preconnectURL?: string
}[]
}
]
chunk
is the unique page name we chose via magic comments. The name "main" cannot be used.
path
is the pathname of the page (like /
, /about
, /posts
...).
data
can be supplied if the page has dynamic data that will be fetched right when it loads.
url
is the data API URL. Can be a string or a function that receives the dynamic path params and returns a string.
For example:
{
chunk: 'products',
path: '/products/:category/:page',
data: {
url: ({ category, page }) => `https://www.my-api.com/products/${category}/${page}`
}
}
crossorigin
is for CORS-enabled fetches.
preconnectURL
is for cases where the fetch request will be followed by requests to resources from a different origin.
You should use @emotion/css as your styling solution.
This package is the perfect balance between potential and performance, between "styled components" and CSS modules.
It can even be used to mimic the API of CSS modules and thus allow for easy migration:
import { css, cx } from '@emotion/css'
import Back from './Back'
const Title = ({ className, back, children, ...otherProps }) => {
return (
<div {...otherProps}>
{back && <Back className={style.back} />}
<h1 className={cx(style.wrapper, className)}>{children}</h1>
</div>
)
}
const style = {
wrapper: css`
font-weight: 500;
color: var(--primary-color);
`,
back: css`
margin-right: 20px;
`
}
export default Title
Note that if you choose to use CSS modules instead, you should NOT extract CSS to separate files (typically done by mini-css-extract-plugin).
Despite the fact that the advantages of this extraction are mostly negligible nowadays, when used together with code-splitting, it will produce severe styling override issues with shared components.