前言
前端性能優(yōu)化可以大致可以從三個方面入手:打包構(gòu)建性能、網(wǎng)絡傳輸性能、運行性能。這三個方面涵蓋了我們從打包部署到用戶使用的全過程。本文將介紹一些具體的實施辦法。
打包構(gòu)建性能
壓縮代碼
代碼壓縮可以減少頁面的加載時間、減少對網(wǎng)絡帶寬的占用、增強代碼安全性。
Webpack 提供了許多壓縮代碼的插件,下面介紹其中兩個常用的插件:
-
UglifyJSPlugin
UglifyJSPlugin 是一個可以將 JS 代碼壓縮至極致的插件。可以通過以下方式來安裝和配置:
安裝:
npm i uglifyjs-webpack-plugin -D
引入:
// webpack.config.js const UglifyJsPlugin = require("uglifyjs-webpack-plugin") module.exports = { plugins: [new UglifyJsPlugin()], }
-
OptimizeCSSAssetsPlugin
OptimizeCSSAssetsPlugin 是一個可以壓縮 CSS 代碼的插件。可以通過以下方式來安裝和配置:
安裝:
npm i optimize-css-assets-webpack-plugin -D
引入:
// webpack.config.js const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") module.exports = { optimization: { minimizer: [new OptimizeCSSAssetsPlugin({})], }, }
通過使用 Webpack 提供的 UglifyJSPlugin 和 OptimizeCSSAssetsPlugin 插件,可以輕松地壓縮 JavaScript 和 CSS 代碼,從而提高頁面加載速度和性能。
Vite 壓縮代碼:
Vite 中默認開啟了代碼壓縮,下面是 Vite 的默認配置,一般情況下不需要修改。
// vite.config.js
import { defineConfig } from "vite"
export default defineConfig({
build: {
minify: "esbuild", // boolean | 'terser' | 'esbuild'
},
})
如果需要,可以通過修改 minify
選項更改壓縮方式。默認為 Esbuild,它比 terser 快 20-40 倍,壓縮率只差 1%-2%。
壓縮打包體積
壓縮打包體積可以使用 GZIP,能大幅度減小打包后的文件大小。
WebPack 開啟 GZIP
使用 compression-webpack-plugin
插件可以在構(gòu)建過程中對文件進行 gzip 壓縮,并生成對應的 .gz 文件。
安裝:
npm i compression-webpack-plugin -D
引入:
// webpack.config.js
const CompressionPlugin = require("compression-webpack-plugin")
module.exports = {
plugins: [
new CompressionPlugin({
algorithm: "gzip",
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),
],
}
在上面的配置中使用了 compression-webpack-plugin
插件對所有的 .js、.css、.html 和 .svg 文件進行 gzip 壓縮,并設置了壓縮的閾值和比率。
Vite 開啟 GZIP
在 Vite 中開啟 gzip,可以使用 vite-plugin-compression
插件來實現(xiàn)。這個插件可以在構(gòu)建過程中對文件進行 gzip 壓縮,并生成對應的 .gz 文件。
安裝:
npm i vite-plugin-compression -D
引入:
// vite.config.js
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import viteCompression from "vite-plugin-compression"
export default defineConfig({
plugins: [
vue(),
viteCompression({
threshold: 10240, // the unit is Bytes
}),
],
})
在這個配置中,我們使用 vite-plugin-compression
插件對所有大于 10KB 的文件進行 gzip 壓縮,并設置了壓縮的閾值。在構(gòu)建過程中,插件會自動在輸出目錄下生成對應的 .gz 文件。然后在 Web 服務器的配置文件中開啟 gzip,當瀏覽器請求文件時,服務器會將對應的 gzip 文件返回給瀏覽器。
更多配置,請參考 ?? vbenjs/vite-plugin-compression: Use gzip or brotli to compress resources
在服務器中開啟 GZIP
開啟 gzip 需要同時確保 Web 服務器已經(jīng)開啟了對應的 gzip 支持。
-
以 nginx 為例:
server { gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_min_length 10240; gzip_comp_level 6; }
-
以 nodejs 為例:
npm i compression
// app.js const compression = require("compression") app.use(compression())
壓縮圖片
WebPack 壓縮圖片
在 Webpack 中,可以通過 image-webpack-loader
對圖片進行壓縮和優(yōu)化處理。
安裝:
npm i image-webpack-loader -D
引入:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "images/",
publicPath: "images/",
},
},
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
quality: 80,
},
pngquant: {
quality: [0.65, 0.9],
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75,
},
},
},
],
},
],
},
}
在這個配置中,我們使用 file-loader
處理圖片文件,然后使用 image-webpack-loader
插件對圖片進行壓縮和優(yōu)化處理,其中 options 可以設置不同的壓縮算法和參數(shù)。
Vite 壓縮圖片
在 Vite 中常用的圖片壓縮插件有 imagemin 和 squoosh,下面以 squoosh
為例:
-
安裝
vite-plugin-squoosh
npm install vite-plugin-squoosh -D
-
配置
vite-plugin-squoosh
// vite.config.js const { defineConfig } = require("vite") const vue = require("@vitejs/plugin-vue") const squooshPlugin = require("vite-plugin-squoosh") module.exports = defineConfig({ plugins: [ vue(), squooshPlugin({ // Specify codec options. codecs: { mozjpeg: { quality: 30, smoothing: 1 }, webp: { quality: 25 }, avif: { cqLevel: 20, sharpness: 1 }, jxl: { quality: 30 }, wp2: { quality: 40 }, oxipng: { level: 3 }, }, // Do not encode .wp2 and .webp files. exclude: /.(wp2|webp)$/, // Encode png to webp. encodeTo: [{ from: /.png$/, to: "webp" }], }), ], })
更多配置請參考 ?? vite-plugin-squoosh
網(wǎng)絡傳輸性能
大量的 HTTP 請求會給服務器和瀏覽器造成一定的壓力,導致前端獲取響應的時間增加,從而造成頁面加載時間過長。優(yōu)化網(wǎng)絡傳輸性能可以從以下幾個方面入手:
圖片使用 LazyLoad 懶加載
懶加載也稱為延遲加載。基本思想是,在頁面加載時只加載當前頁面可視區(qū)域的圖片,而其它圖片等用戶滾動到可視區(qū)域頁面時再進行加載。
Vue 中實現(xiàn)圖片懶加載
在 Vue 中實現(xiàn)圖片懶加載,可以使用 vue-lazyload
插件。
-
安裝
vue-lazyload
npm i vue-lazyload
-
引入
vue-lazyload
// main.js import Vue from "vue" import VueLazyload from "vue-lazyload" Vue.use(VueLazyload)
-
在需要懶加載的圖片上使用
v-lazy
指令<template> <img v-lazy="imgUrl" alt="圖片" /> </template>
更多配置點擊這里 ?? vue-lazyload。
React 中實現(xiàn)圖片懶加載
在 React 中實現(xiàn)圖片懶加載,可以借助 react-lazyload
。
-
安裝
react-lazyload
npm i react-lazyload
-
在組件中引入 react-lazyload
import React from "react" import LazyLoad from "react-lazyload" function MyComponent() { return ( <div> <LazyLoad> <img src='path/to/image' alt='image' /> </LazyLoad> </div> ) }
更多配置點擊這里 ?? react-lazyload。
原生 JS 實現(xiàn)圖片懶加載
原生 JS 實現(xiàn)圖片懶加載可以使用 Element.getBoundingClientRect()
方法,該方法會返回元素的大小及其相對于視口的位置。然后判斷圖片是否出現(xiàn)在頁面上。
<img class="lazyload" data-src="https://picsum.photos/200/300" />
<img class="lazyload" data-src="https://picsum.photos/200/300" />
<img class="lazyload" data-src="https://picsum.photos/200/300" />
// 獲取需要懶加載的圖片元素
const lazyloadImages = document.querySelectorAll(".lazyload")
// 獲取視口高度和寬度
const windowHeight = window.innerHeight
const windowWidth = window.innerWidth
// 判斷一個元素是否在視口內(nèi)
function isInViewport(element) {
const rect = element.getBoundingClientRect()
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowHeight && rect.right <= windowWidth
}
// 監(jiān)聽窗口的滾動事件
window.addEventListener("scroll", function () {
lazyloadImages.forEach(img => {
if (isInViewport(img)) {
img.src = img.dataset.src
img.classList.remove("lazyload")
}
})
})
使用 CSS3 代替簡單圖片
可以利用 CSS3 代替一些簡單的圖片,這樣就可以減少一些圖片請求,提高頁面加載速度。
使用 Data URI
將小圖片轉(zhuǎn)為 Data URI 格式,直接嵌入 CSS 或 HTML 中,可以避免請求。但是需要注意,Data URI 格式的圖片會增加頁面大小,需要根據(jù)實際情況進行權衡和選擇。下面介紹在 Webpack 和 Vite 中將小圖片轉(zhuǎn)換為 base64 編碼的 Data URI 格式的方法。
-
Webpack 中將小圖片轉(zhuǎn)換為 base64
在 Webpack 中,可以使用 url-loader 或者 file-loader 將小圖片轉(zhuǎn)換成 base64 編碼的 Data URI 格式。這兩個 loader 都是用于處理文件的,可以將文件轉(zhuǎn)換成模塊,以便在代碼中引用。
其中,url-loader 和 file-loader 的主要區(qū)別在于處理方式不同。url-loader 在處理圖片時,會先判斷圖片大小是否超過指定的限制,如果超過了限制,則使用 file-loader 進行處理;如果沒有超過限制,則將圖片轉(zhuǎn)換成 base64 編碼的 Data URI 格式,并嵌入到代碼中,以減少 HTTP 請求次數(shù)。
// webpack.config.js module.exports = { module: { rules: [ { test: /\.(png|jpg|gif)$/, use: [ { loader: "url-loader", options: { limit: 8192, // 小于 8KB 的圖片會被轉(zhuǎn)換成 base64 編碼的 Data URI 格式 fallback: "file-loader", // 超過 8KB 的圖片使用 file-loader 進行處理 }, }, ], }, ], }, }
-
Vite 中將小圖片轉(zhuǎn)換為 base64
在 Vite 中,可以通過配置
assetsInlineLimit
選項來指定大小限制,小于指定大小的圖片將被轉(zhuǎn)換成 base64 編碼的 Data URI 格式。下面是在 Vite 中配置
assetsInlineLimit
的示例:// vite.config.js module.exports = { build: { assetsInlineLimit: 8192, // 小于 8KB 的圖片會被轉(zhuǎn)換成 base64 編碼的 Data URI 格式 }, }
使用雪碧圖或字體圖標代替圖標圖片
將多個小圖標合并成一張圖片,通過 CSS 背景定位來顯示不同的圖片,減少請求次數(shù)。
或者使用字體圖標來代替圖標圖片,減少請求次數(shù)。
合并文件
將多個 CSS 文件或 JavaScript 文件合并成一個文件,減少請求次數(shù)。
使用 HTTP/2
HTTP/2 是一種新的協(xié)議,相比 HTTP/1.1,可以更快地傳輸數(shù)據(jù)。 HTTP/1.1 的 Headers 采用的是文本格式,并且每一次請求都會帶上一些完全相同的數(shù)據(jù),而 HTTP/2 采用的是二進制編碼,并且對 Headers 進行了 HPack 壓縮,進而提升了傳輸效率。不僅如此,HTTP/2 還可以同時發(fā)送多個請求和響應,因此可以通過合并和壓縮資源,減少請求的數(shù)量,提高頁面加載速度。
HTTP/2 是向下兼容的,當瀏覽器不支持的時候會自動切換到 HTTP/1.1。但需要在服務器端和客戶端同時配置才能生效。
可以通過以下步驟來升級到 HTTP/2:
使用 HTTPS:HTTP/2 只支持加密連接,因此需要使用 HTTPS 來使用 HTTP/2。
在服務器端啟用 HTTP/2:需要在服務器端啟用 HTTP/2,以便前端可以使用該協(xié)議。常見的 Web 服務器,如 Nginx 和 Apache,都支持 HTTP/2。
Nginx 啟用 HTTP/2 請參考這里 ?? Supporting HTTP/2 for Google Chrome Users | NGINX
另外,關于 HTTP 版本之間的區(qū)別,可以參考 ?? 了解 HTTP 的前世今生。
異步加載 JS 和 CSS
使用異步加載可以提高頁面響應速度,具體來說,可以采取以下措施:
將 JavaScript 腳本放在頁面底部,或者使用 defer 或 async 屬性延遲 JavaScript 加載和執(zhí)行。
將 CSS 樣式通過 link 標簽異步加載,也可以使用 JavaScript 動態(tài)加載樣式表。
使用 CDN
服務器的位置是固定的,負載也是有限的。通常訪客區(qū)域距離服務器越遠,打開網(wǎng)站速度越慢。如果在高峰時間段,網(wǎng)站訪問量很大,服務器無法負載,也會導致訪問速度下降。
因此,我們可以將某些資源放到 CDN 上,這樣就可以減少對服務器的 HTTP 請求。從而提高頁面加載速度。
運行性能
避免重繪(Repaint)和重排(Reflow)
重繪和重排都會導致頁面或部分頁面重新渲染,所以我們應當盡量避免觸發(fā)這兩個瀏覽器事件。
重繪(Repaint) 就是瀏覽器使用新的樣式重新渲染一個元素。改變元素的以下樣式通常會觸發(fā)重繪,應當盡量避免:
background
border
border-radius
box-shadow
color
visibility
outline
如果需要改變元素的某些 CSS 屬性,盡量一次性改變,減少觸發(fā)重繪的次數(shù)。
重排(Reflow) 就是瀏覽器重新渲染部分或全部頁面,通常是當元素的尺寸發(fā)生改變或者瀏覽器的一些行為影響到頁面布局而觸發(fā)的,比如
clientWidth
、clientHeight
等窗口屬性改變box-sizing
、width
、height
、border
、padding
等元素尺寸改變font-size
、line-height
等元素字體大小改變margin
、float
、flex-direction
等元素位置改變
如果需要改變元素位置或尺寸,可以使用以下屬性避免重排:
position: fixed | absolute
使元素脫離文檔流。transform
屬性不會觸發(fā)避免重排。
使用節(jié)流防抖
在用戶瀏覽網(wǎng)頁或者在網(wǎng)頁上進行一些操作時,我們常常需要監(jiān)聽一些事件去完成相應的功能。比如鼠標點擊、鍵盤輸入、滾輪滾動等一些其它事件。
在處理這些事件時,我們常常會發(fā)現(xiàn),如果一個事件發(fā)生的頻率過高,相應的代碼執(zhí)行的頻率也會越高。
所以,我們并不需要代碼如此高頻率的執(zhí)行,這在一定程度上對系統(tǒng)資源造成了浪費,程序的性能也會因此變得很差。因此,我們需要使用節(jié)流防抖,對代碼的執(zhí)行頻率進行一些限制。
基本的節(jié)流函數(shù)
/**
* @description 節(jié)流函數(shù)
* @param {Function} func 需要節(jié)流的函數(shù)
* @param {Number} wait 節(jié)流的時間
* @returns {Function} 返回節(jié)流后的函數(shù)
*/
function throttle(func, wait) {
let timer = null
return function () {
if (timer) return
timer = setTimeout(() => {
func.apply(this, arguments)
timer = null
}, wait)
}
}
基本的防抖函數(shù)
/**
* @description 防抖函數(shù)
* @param {Function} func 需要防抖的函數(shù)
* @param {Number} wait 防抖的時間
* @returns {Function} 返回防抖后的函數(shù)
*/
function debounce(func, wait) {
let timeout
return function () {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, arguments)
}, wait)
}
}
使用示例
// 節(jié)流
input.addEventListener("input", throttle(printInputText, 500))
// 防抖
input.addEventListener("input", debounce(printInputText, 500))
更多有關節(jié)流防抖的使用,可以參考 ?? 手寫實現(xiàn)節(jié)流防抖
使用服務端渲染(SSR)
SSR(Server-Side Rendering,服務端渲染)是指將 Web 頁面的生成過程從瀏覽器端轉(zhuǎn)移到服務器端完成的一種技術。
在傳統(tǒng)的 SPA(Single Page Application,單頁應用)中,瀏覽器需要先下載 HTML、CSS、JavaScript 文件,并在客戶端執(zhí)行 JavaScript 代碼,才能生成頁面內(nèi)容。這樣的過程需要加載大量的 JavaScript 代碼,會導致首屏渲染時間較長,影響用戶體驗。而通過 SSR 技術,服務器可以將頁面的 HTML、CSS 和 JavaScript 代碼預先生成好,并將渲染好的 HTML 代碼直接返回給瀏覽器,從而加快頁面加載速度,提高用戶體驗。
SSR 在性能提升方面有以下優(yōu)點:
提高頁面加載速度:SSR 可以將渲染頁面的工作從瀏覽器端轉(zhuǎn)移到服務器端,減少瀏覽器的工作量,從而加快頁面加載速度。
提高首屏渲染速度:SSR 可以將頁面的渲染過程提前到服務器端完成,使得頁面在瀏覽器端顯示的速度更快,從而提高用戶體驗。
更好的可訪問性:通過 SSR 技術,我們可以生成更符合 Web 標準的 HTML 頁面,從而使得頁面具有更好的可訪問性。
有關 SSR 的更多介紹,可以參考 五分鐘了解 SPA 與 SSR
使用 Web Workers
使用 Web Workers 可以將一些任務放在后臺線程中執(zhí)行,以避免阻塞主線程。主線程通常用于處理用戶界面的更新和響應用戶的操作,如果在主線程中執(zhí)行耗時的任務,會導致用戶界面出現(xiàn)卡頓或失去響應。使用 Web Workers 可以將這些耗時的任務放在后臺線程中執(zhí)行,從而避免阻塞主線程。Web Workers 可以與主線程進行通信,使得后臺線程可以向主線程發(fā)送消息,而主線程也可以向后臺線程發(fā)送消息。這種通信可以通過 postMessage 方法實現(xiàn)。
Web Workers 的使用有一定的限制,例如它們不能訪問 DOM 和一些瀏覽器 API。但是,如果應用程序需要處理大量數(shù)據(jù)或進行復雜的計算,使用 Web Workers 可以提高應用程序的性能和響應速度。
以下是一個使用 Web Workers 的實際案例和代碼示例:
假設我們有一個應用程序需要處理大量的數(shù)據(jù),例如計算數(shù)組中所有元素的平均值。由于數(shù)據(jù)量很大,如果在主線程中執(zhí)行這個任務,會導致頁面出現(xiàn)卡頓或失去響應。因此,我們可以使用 Web Workers 將這個任務放在后臺線程中執(zhí)行,從而避免阻塞主線程。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Web Workers Example</title>
</head>
<body>
<p>計算數(shù)組中所有元素的平均值:</p>
<p id="result"></p>
<script>
// 創(chuàng)建 Web Worker
const worker = new Worker("worker.js")
// 發(fā)送消息給 Web Worker
worker.postMessage([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// 監(jiān)聽 Web Worker 的消息
worker.onmessage = function (event) {
// 將計算結(jié)果顯示在頁面上
document.getElementById("result").textContent = event.data
}
</script>
</body>
</html>
// worker.js
// 監(jiān)聽主線程的消息
onmessage = function (event) {
// 計算數(shù)組中所有元素的平均值
const sum = event.data.reduce((a, b) => a + b, 0)
const average = sum / event.data.length
// 發(fā)送計算結(jié)果給主線程
postMessage(average)
}
參考資料:
特別感謝:
- ChatGPT