Enable gzip or brotli compression#
Generally, if static resources have been uploaded to a CDN through static-dynamic separation, the CDN will usually have enabled 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 request size of images, thereby reducing loading time and improving performance.
If the service is deployed on the Vercel platform, the images will be optimized by default.
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, which can avoid 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 on domain name resolution and TCP handshakes. We can significantly improve page loading speed by adding dns-prefetch and preconnect meta tags to complete these operations in advance.
<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 code below 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 content related to time, 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)
);
- Dynamic 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 />;
}
- Actually render the final content 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 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.