Shipping only modern JavaScript
Next.js currently targets ES5 for JS output and autoprefixes CSS in order to support legacy browsers. This results in much larger JS and CSS output because newer features need to be down-leveled. You can opt-out of this behavior with experimental flags below:
const nextConfig = {
experimental: {
legacyBrowsers: false,
browsersListForSwc: true
}
}
module.exports = nextConfig
Use next/future/image
The next/future/image
component improves both the performance and developer experience of next/image
by using the native <img>
element with better default behavior.
This component uses browser native lazy loading.
// Before
import Image from 'next/image'
// After
import Image from 'next/future/image'
You might need to tweak some properties, because layout
is not available.
Use next/dynamic
imports
Next.js supports lazy loading external libraries with import()
and React components with next/dynamic
. Deferred loading helps improve the initial loading performance by decreasing the amount of JavaScript necessary to render the page. Components or libraries are only imported and included in the JavaScript bundle when they're used.
next/dynamic
is an extension of React.lazy
. When used in combination with Suspense
, components can delay hydration until the Suspense boundary is resolved.
By using next/dynamic
, the header component will not be included in the page's initial JavaScript bundle. The page will render the Suspense fallback
first, followed by the Header
component when the Suspense
boundary is resolved.
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
const DynamicHeader = dynamic(() => import('../components/header'), {
suspense: true,
})
export default function Home() {
return (
<Suspense fallback={`Loading...`}>
<DynamicHeader />
</Suspense>
)
}
We can also defer loading external libraries until module is needed, a.k.a user types in the search input.
This example uses fuse.js
.
import { useState } from 'react'
const names = ['Tim', 'Joe', 'Bel', 'Lee']
export default function Page() {
const [results, setResults] = useState()
return (
<div>
<input
type="text"
placeholder="Search"
onChange={async (e) => {
const { value } = e.currentTarget
// Dynamically load fuse.js
const Fuse = (await import('fuse.js')).default
const fuse = new Fuse(names)
setResults(fuse.search(value))
}}
/>
<pre>Results: {JSON.stringify(results, null, 2)}</pre>
</div>
)
}
Replace react
with preact
Preact
is a fast 3kB alternative to React with the same modern API. It doesn't ship Virtual DOM and uses native browser APIs. You can give it a shot, but some packages might not work and you might run into some issues. Make sure to test changes before deploying to production!
Here is basic package.json
needed for replacing React with Preact
{
"scripts": {
...
},
"dependencies": {
"next": "latest",
"next-plugin-preact": "latest",
"preact": "latest",
"preact-render-to-string": "latest",
"react": "npm:@preact/compat",
"react-dom": "npm:@preact/compat",
"react-ssr-prepass": "npm:preact-ssr-prepass"
},
"devDependencies": {
"@types/node": "latest",
"@types/react": "latest",
"typescript": "latest"
}
}
Then you need to tweak your next.config.js
to use next-plugin-preact
:
const withPreact = require('next-plugin-preact')
/** @type {import('next').NextConfig} */
const nextConfig = {
/* regular next.js config options here */
}
module.exports = withPreact(nextConfig)
And that's it. There is no need to replace your imports, because preact
has a combability layer with react
.
Here is a bundle size comparison:
- React:
Route (pages) Size First Load JS
┌ ○ / 2.39 kB 79.6 kB
├ ○ /404 194 B 77.4 kB
├ ○ /about 274 B 77.5 kB
├ ● /ssg 305 B 77.5 kB
└ λ /ssr 305 B 77.5 kB
+ First Load JS shared by all 77.2 kB
├ chunks/framework-7dc8a65f4a0cda33.js 45.2 kB
├ chunks/main-18053c3f67c4d467.js 31 kB
├ chunks/pages/_app-dc14f8483464b560.js 201 B
└ chunks/webpack-69bfa6990bb9e155.js 769 B
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
- Preact:
Route (pages) Size First Load JS
┌ ○ / 2.4 kB 43 kB
├ ○ /404 194 B 40.8 kB
├ ○ /about 271 B 40.9 kB
├ ● /ssg 305 B 40.9 kB
└ λ /ssr 303 B 40.9 kB
+ First Load JS shared by all 40.6 kB
├ chunks/framework-f2746757f5385dad.js 8.73 kB
├ chunks/main-632edec26452657f.js 30.9 kB
├ chunks/pages/_app-dc14f8483464b560.js 201 B
└ chunks/webpack-69bfa6990bb9e155.js 769 B
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
As you can see, we stripped out about 37kB of JS.
Because of that, browser doesn't have to interpret a lot of JS (37kB is getting unzipped) which will lead to performance gains.