開啟 gzip 或者 brotli 壓縮#
一般如果動靜分離已經將靜態資源上傳到了 CDN,正常 CDN 都是開啟了以上的壓縮方案的
如果沒有開啟需要開啟相關能力
圖片壓縮#
儘量使用 webp 格式圖片和壓縮圖片,可以減少圖片的請求大小,從而減少加載時間,提升性能。
如果是部署於 vercel 平台的服務圖片默認開啟相關的優化邏輯
圖片懶加載#
圖片懶加載是一種常見的提升網頁性能的技術,它會等到圖片進入視窗再開始加載它們,可以避免一開始加載過多的資源。
<Image loading="lazy" src={renderItem.img} alt="" fill />
添加 dns-prefetch、preconnect meta 標籤#
DNS 預獲取 (dns-prefetch) 和預連接 (preconnect) 是兩種讓瀏覽器提前進行域名解析和建立 TCP 握手的技術
在加載一個新的頁面時,瀏覽器需要花費一定的時間來解析域名和建立 TCP 握手。我們可以通過添加 dns-prefetch 和 preconnect meta 標籤來預先完成這些操作,從而顯著提高頁面加載速度
<Head>
<link rel="dns-prefetch" href="//example.com" />
<link rel="preconnect" href="//example.com" />
</Head>
自部署 next 服務的自定義圖片 loader 優化#
下面的代碼展示了一個自定義的圖片 loader,它可以將圖片優化的功能外包到另一個服務。
'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 }) => {
// 確認一個可用的圖片優化服務之後替換
return `https://optimize-img.com?url=${src}?w=${width}&q=${quality || 75}`;
}}
/>
);
};
部分邏輯組件和渲染內容獨立於服務端渲染#
有些組件作為用戶交互之後才需要展示的內容,例如預留好插槽的 Modal 組件,類似這樣的組件沒有伺服器端渲染的必要,可以考慮修改他的實現為僅客戶端渲染。同樣,如果你正在渲染和時間有關的內容,由於伺服器和客戶端的時間可能存在微小差異,也可能導致錯誤。
這可以通過以下方法實行:
- 使用 next 提供的 dynamic
import dynamic from 'next/dynamic';
//僅在客戶端動態導入並渲染 Modal 組件
const DynamicModal = dynamic(
() => import('../components/Modal'),
{ ssr: false } // 這將禁用伺服器端渲染(SSR)
);
// 僅在客戶端動態導入並渲染 Time 相關組件
const DynamicTime = dynamic(
() => import('../components/Time'),
{ ssr: false } // 這將禁用伺服器端渲染(SSR)
);
- useEffect 中動態引入
import React, { useState, useEffect } from 'react';
function YourComponent() {
const [DynamicComponent, setDynamicComponent] = useState(null);
useEffect(() => {
import('../components/Modal').then((Modal) => {
setDynamicComponent(() => Modal.default); // 注意 Modal.default
});
}, []);
if (!DynamicComponent) return <p>Loading...</p>;
return <DynamicComponent />;
}
- 首幀之後才實際渲染最終內容
import React, { useState, useEffect } from 'react';
import Modal from '../components/Modal';
function YourComponent() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
// 在 useEffect 中設置標誌表示現在是在客戶端了
setIsClient(true);
}, []);
if (!isClient) return <p>Loading...</p>;
return <Modal />;
}
打包靜態資源上傳到 CDN 的插件#
以下代碼展示了如何將打包後的靜態文件上傳到 CDN,這樣可以顯著減少伺服器的負載,同時由於 CDN 通常都具備很好的地理分佈和加載優化,所以使用 CDN 可以進一步提升用戶的加載體驗。
/* 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;
// ...