Enable gzip or brotli compression#
Generally, if static resources have been uploaded to a CDN through static-dynamic separation, the CDN will usually enable the above compression schemes. If not, you need to enable the relevant capabilities.
Image compression#
Try to use webp format images and compressed images to reduce the size of image requests, thereby reducing loading time and improving performance.
If the service is deployed on the Vercel platform, the default optimization logic for images is enabled.
Image lazy loading#
Image lazy loading is a common technique to improve webpage performance. It waits until the images enter the viewport before loading them, avoiding loading too many resources at the beginning.
<Image loading="lazy" src={renderItem.img} alt="" fill />
Add dns-prefetch and preconnect meta tags#
DNS prefetching (dns-prefetch) and preconnecting (preconnect) are two techniques that allow the browser to perform domain name resolution and establish TCP handshakes in advance. When loading a new page, the browser needs to spend some time resolving the domain name and establishing a TCP handshake. We can add dns-prefetch and preconnect meta tags to precomplete these operations in advance, significantly improving page loading speed.
<Head>
<link rel="dns-prefetch" href="//example.com" />
<link rel="preconnect" href="//example.com" />
</Head>
Custom image loader optimization for self-deployed Next.js services#
The following code shows a custom image loader that outsources the image optimization functionality to another service.
'use client'
import React, { ComponentProps } from 'react';
import Image from 'next/image';
export const CustomLoaderImg = (props: ComponentProps<typeof Image>) => {
return (
<Image
{...props}
loader={({ src, width, quality }) => {
// Replace with a usable image optimization service
return `https://optimize-img.com?url=${src}?w=${width}&q=${quality || 75}`;
}}
/>
);
};
Separate logic components and rendering content from server-side rendering#
Some components only need to be displayed after user interaction, such as a Modal component with reserved slots. Such components do not require server-side rendering and can be modified to be rendered only on the client-side. Similarly, if you are rendering time-related content, slight differences in time between the server and the client may cause errors.
This can be done in the following ways:
- Use the dynamic provided by Next.js
import dynamic from 'next/dynamic';
// Dynamically import and render the Modal component only on the client-side
const DynamicModal = dynamic(
() => import('../components/Modal'),
{ ssr: false } // This will disable server-side rendering (SSR)
);
// Dynamically import and render Time-related components only on the client-side
const DynamicTime = dynamic(
() => import('../components/Time'),
{ ssr: false } // This will disable server-side rendering (SSR)
);
- Dynamically import in useEffect
import React, { useState, useEffect } from 'react';
function YourComponent() {
const [DynamicComponent, setDynamicComponent] = useState(null);
useEffect(() => {
import('../components/Modal').then((Modal) => {
setDynamicComponent(() => Modal.default); // Note Modal.default
});
}, []);
if (!DynamicComponent) return <p>Loading...</p>;
return <DynamicComponent />;
}
- Render the final content only after the first frame
import React, { useState, useEffect } from 'react';
import Modal from '../components/Modal';
function YourComponent() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
// Set a flag in useEffect to indicate that it is now on the client-side
setIsClient(true);
}, []);
if (!isClient) return <p>Loading...</p>;
return <Modal />;
}
Plugin for packaging static resources and uploading them to a CDN#
The following code shows how to upload packaged static files to a CDN, which can significantly reduce the server load. Additionally, using a CDN with good geographical distribution and loading optimization can further improve the user's loading experience.
/* eslint-disable react-func/max-lines-per-function */
/* eslint-disable max-depth */
const request = require('request');
function getCdnUrlPrefix(options) {
return `${options.packageVersion}/${options?.isEnvProduction ? 'prod' : 'dev'}`;
}
function UploadCDN(options) {
this.isEnvProduction = options?.isEnvProduction ?? false;
this.options = options;
}
function upload(options) {
return new Promise((resolve, reject) => {
request(options, function (error, response) {
if (error) reject(error);
resolve();
});
});
}
UploadCDN.prototype.apply = function (compiler) {
// eslint-disable-next-line react-func/max-lines-per-function
compiler.hooks.emit.tapAsync('UploadCDN', async (compilation, callback) => {
// formData list
let optionsMap = {};
let commonOptions = {
method: 'POST',
url: 'https://your-cdn-serverice/api/v1/files',
headers: {
module: this.options.module,
_dir: '/',
'Content-Type': 'application/x-www-form-urlencoded',
},
formData: {},
};
for (let fileName in compilation.assets) {
if (!fileName.endsWith('map')) {
const parts = fileName.split('/');
const filename = parts.pop();
const _dir = `${getCdnUrlPrefix(this.options)}/_next/${parts.join('/')}`;
const formData = {
value: compilation.assets[fileName].source(),
options: {
filename,
contentType: null,
},
};
if (!optionsMap[_dir]) {
optionsMap[_dir] = {
...JSON.parse(JSON.stringify(commonOptions)),
};
optionsMap[_dir].headers._dir = _dir;
}
optionsMap[_dir].formData[filename] = formData;
}
}
const optionsArr = Object.keys(optionsMap).map(key => optionsMap[key]);
optionsArr.forEach(async options => await upload(options));
callback();
});
};
module.exports = {
UploadCDN,
};
// next.config.js
// ...
assetPrefix: isProd ? `https://your-cdn-serverice/${yourModule}/${moduleName}/${packageVersion}/prod` : undefined,
webpack: (config, context) => {
isProd &&
config.plugins.push(
new UploadCDN({
module: moduleName,
packageVersion,
isEnvProduction: isProd,
}),
);
return config;
// ...
Recommended Reading#
Next.js Official Documentation
Google - Web Performance Optimization Guide.