前端效能最佳化 24 條建議(2020)

效能最佳化是把雙刃劍,有好的一面也有壞的一面。好的一面就是能提升網站效能,壞的一面就是配置麻煩,或者要遵守的規則太多。並且某些效能最佳化規則並不適用所有場景,需要謹慎使用,請讀者帶著批判性的眼光來閱讀本文。

本文相關的最佳化建議的引用資料出處均會在建議後面給出,或者放在文末(有些參考資料可能要梯子才能觀看)。

1。 減少 HTTP 請求

一個完整的 HTTP 請求需要經歷 DNS 查詢,TCP 握手,瀏覽器發出 HTTP 請求,伺服器接收請求,伺服器處理請求併發迴響應,瀏覽器接收響應等過程。接下來看一個具體的例子幫助理解 HTTP :

前端效能最佳化 24 條建議(2020)

這是一個 HTTP 請求,請求的檔案大小為 28。4KB。

名詞解釋:

Queueing: 在請求佇列中的時間。

Stalled: 從TCP 連線建立完成,到真正可以傳輸資料之間的時間差,此時間包括代理協商時間。

Proxy negotiation: 與代理伺服器連線進行協商所花費的時間。

DNS Lookup: 執行DNS查詢所花費的時間,頁面上的每個不同的域都需要進行DNS查詢。

Initial Connection / Connecting: 建立連線所花費的時間,包括TCP握手/重試和協商SSL。

SSL: 完成SSL握手所花費的時間。

Request sent: 發出網路請求所花費的時間,通常為一毫秒的時間。

Waiting(TFFB): TFFB 是發出頁面請求到接收到應答資料第一個位元組的時間。

Content Download: 接收響應資料所花費的時間。

從這個例子可以看出,真正下載資料的時間佔比為

13。05 / 204。16 = 6。39%

,檔案越小,這個比例越小,檔案越大,比例就越高。這就是為什麼要建議將多個小檔案合併為一個大檔案,從而減少 HTTP 請求次數的原因。

參考資料:

understanding-resource-timing

2。 使用 HTTP2

HTTP2 相比 HTTP1。1 有如下幾個優點:

解析速度快

伺服器解析 HTTP1。1 的請求時,必須不斷地讀入位元組,直到遇到分隔符 CRLF 為止。而解析 HTTP2 的請求就不用這麼麻煩,因為 HTTP2 是基於幀的協議,每個幀都有表示幀長度的欄位。

多路複用

HTTP1。1 如果要同時發起多個請求,就得建立多個 TCP 連線,因為一個 TCP 連線同時只能處理一個 HTTP1。1 的請求。

在 HTTP2 上,多個請求可以共用一個 TCP 連線,這稱為多路複用。同一個請求和響應用一個流來表示,並有唯一的流 ID 來標識。 多個請求和響應在 TCP 連線中可以亂序傳送,到達目的地後再透過流 ID 重新組建。

首部壓縮

HTTP2 提供了首部壓縮功能。

例如有如下兩個請求:

// 請求1:authority: unpkg。zhimg。com:method: GET:path: /za-js-sdk@2。16。0/dist/zap。js:scheme: httpsaccept: */*accept-encoding: gzip, deflate, braccept-language: zh-CN,zh;q=0。9cache-control: no-cachepragma: no-cachereferer: https://www。zhihu。com/sec-fetch-dest: scriptsec-fetch-mode: no-corssec-fetch-site: cross-siteuser-agent: Mozilla/5。0 (Windows NT 6。1; Win64; x64) AppleWebKit/537。36 (KHTML, like Gecko) Chrome/80。0。3987。122 Safari/537。36// 請求2:authority: zz。bdstatic。com:method: GET:path: /linksubmit/push。js:scheme: httpsaccept: */*accept-encoding: gzip, deflate, braccept-language: zh-CN,zh;q=0。9cache-control: no-cachepragma: no-cachereferer: https://www。zhihu。com/sec-fetch-dest: scriptsec-fetch-mode: no-corssec-fetch-site: cross-siteuser-agent: Mozilla/5。0 (Windows NT 6。1; Win64; x64) AppleWebKit/537。36 (KHTML, like Gecko) Chrome/80。0。3987。122 Safari/537。36

從上面兩個請求可以看出來,有很多資料都是重複的。如果可以把相同的首部儲存起來,僅傳送它們之間不同的部分,就可以節省不少的流量,加快請求的時間。

HTTP/2 在客戶端和伺服器端使用“首部表”來跟蹤和儲存之前傳送的鍵-值對,對於相同的資料,不再透過每次請求和響應傳送。

下面再來看一個簡化的例子,假設客戶端按順序傳送如下請求首部:

Header1:fooHeader2:barHeader3:bat

當客戶端傳送請求時,它會根據首部值建立一張表:

前端效能最佳化 24 條建議(2020)

如果伺服器收到了請求,它會照樣建立一張表。 當客戶端傳送下一個請求的時候,如果首部相同,它可以直接傳送這樣的首部塊:

62 63 64

伺服器會查詢先前建立的表格,並把這些數字還原成索引對應的完整首部。

優先順序

HTTP2 可以對比較緊急的請求設定一個較高的優先順序,伺服器在收到這樣的請求後,可以優先處理。

流量控制

由於一個 TCP 連線流量頻寬(根據客戶端到伺服器的網路頻寬而定)是固定的,當有多個請求併發時,一個請求佔的流量多,另一個請求佔的流量就會少。流量控制可以對不同的流的流量進行精確控制。

伺服器推送

HTTP2 新增的一個強大的新功能,就是伺服器可以對一個客戶端請求傳送多個響應。換句話說,除了對最初請求的響應外,伺服器還可以額外向客戶端推送資源,而無需客戶端明確地請求。

例如當瀏覽器請求一個網站時,除了返回 HTML 頁面外,伺服器還可以根據 HTML 頁面中的資源的 URL,來提前推送資源。

現在有很多網站已經開始使用 HTTP2 了,例如知乎:

前端效能最佳化 24 條建議(2020)

其中 h2 是指 HTTP2 協議,http/1。1 則是指 HTTP1。1 協議。

參考資料:

HTTP2 簡介

半小時搞懂 HTTP、HTTPS和HTTP2

3。 使用服務端渲染

客戶端渲染: 獲取 HTML 檔案,根據需要下載 JavaScript 檔案,執行檔案,生成 DOM,再渲染。

服務端渲染:服務端返回 HTML 檔案,客戶端只需解析 HTML。

優點:首屏渲染快,SEO 好。

缺點:配置麻煩,增加了伺服器的計算壓力。

下面我用 Vue SSR 做示例,簡單的描述一下 SSR 過程。

客戶端渲染過程

訪問客戶端渲染的網站。

伺服器返回一個包含了引入資源語句和

的 HTML 檔案。

客戶端透過 HTTP 向伺服器請求資源,當必要的資源都載入完畢後,執行

new Vue()

開始例項化並渲染頁面。

服務端渲染過程

訪問服務端渲染的網站。

伺服器會檢視當前路由元件需要哪些資原始檔,然後將這些檔案的內容填充到 HTML 檔案。如果有 ajax 請求,就會執行它進行資料預取並填充到 HTML 檔案裡,最後返回這個 HTML 頁面。

當客戶端接收到這個 HTML 頁面時,可以馬上就開始渲染頁面。與此同時,頁面也會載入資源,當必要的資源都載入完畢後,開始執行

new Vue()

開始例項化並接管頁面。

從上述兩個過程中可以看出,區別就在於第二步。客戶端渲染的網站會直接返回 HTML 檔案,而服務端渲染的網站則會渲染完頁面再返回這個 HTML 檔案。

這樣做的好處是什麼?是更快的內容到達時間 (time-to-content)

假設你的網站需要載入完 abcd 四個檔案才能渲染完畢。並且每個檔案大小為 1 M。

這樣一算:客戶端渲染的網站需要載入 4 個檔案和 HTML 檔案才能完成首頁渲染,總計大小為 4M(忽略 HTML 檔案大小)。而服務端渲染的網站只需要載入一個渲染完畢的 HTML 檔案就能完成首頁渲染,總計大小為已經渲染完畢的 HTML 檔案(這種檔案不會太大,一般為幾百K,我的個人部落格網站(SSR)載入的 HTML 檔案為 400K)。

這就是服務端渲染更快的原因

參考資料:

vue-ssr-demo

Vue。js 伺服器端渲染指南

4。 靜態資源使用 CDN

內容分發網路(CDN)是一組分佈在多個不同地理位置的 Web 伺服器。我們都知道,當伺服器離使用者越遠時,延遲越高。CDN 就是為了解決這一問題,在多個位置部署伺服器,讓使用者離伺服器更近,從而縮短請求時間。

CDN 原理

當用戶訪問一個網站時,如果沒有 CDN,過程是這樣的:

瀏覽器要將域名解析為 IP 地址,所以需要向本地 DNS 發出請求。

本地 DNS 依次向根伺服器、頂級域名伺服器、許可權伺服器發出請求,得到網站伺服器的 IP 地址。

本地 DNS 將 IP 地址發回給瀏覽器,瀏覽器向網站伺服器 IP 地址發出請求並得到資源。

前端效能最佳化 24 條建議(2020)

如果使用者訪問的網站部署了 CDN,過程是這樣的:

瀏覽器要將域名解析為 IP 地址,所以需要向本地 DNS 發出請求。

本地 DNS 依次向根伺服器、頂級域名伺服器、許可權伺服器發出請求,得到全域性負載均衡系統(GSLB)的 IP 地址。

本地 DNS 再向 GSLB 發出請求,GSLB 的主要功能是根據本地 DNS 的 IP 地址判斷使用者的位置,篩選出距離使用者較近的本地負載均衡系統(SLB),並將該 SLB 的 IP 地址作為結果返回給本地 DNS。

本地 DNS 將 SLB 的 IP 地址發回給瀏覽器,瀏覽器向 SLB 發出請求。

SLB 根據瀏覽器請求的資源和地址,選出最優的快取伺服器發回給瀏覽器。

瀏覽器再根據 SLB 發回的地址重定向到快取伺服器。

如果快取伺服器有瀏覽器需要的資源,就將資源發回給瀏覽器。如果沒有,就向源伺服器請求資源,再發給瀏覽器並快取在本地。

前端效能最佳化 24 條建議(2020)

參考資料:

CDN是什麼?使用CDN有什麼優勢?

CDN原理簡析

5。 將 CSS 放在檔案頭部,JavaScript 檔案放在底部

所有放在 head 標籤裡的 CSS 和 JS 檔案都會堵塞渲染。如果這些 CSS 和 JS 需要載入和解析很久的話,那麼頁面就空白了。所以 JS 檔案要放在底部,等 HTML 解析完了再載入 JS 檔案。

那為什麼 CSS 檔案還要放在頭部呢?

因為先載入 HTML 再載入 CSS,會讓使用者第一時間看到的頁面是沒有樣式的、“醜陋”的,為了避免這種情況發生,就要將 CSS 檔案放在頭部了。

另外,JS 檔案也不是不可以放在頭部,只要給 script 標籤加上 defer 屬性就可以了,非同步下載,延遲執行。

6。 使用字型圖示 iconfont 代替圖片圖示

字型圖示就是將圖示製作成一個字型,使用時就跟字型一樣,可以設定屬性,例如 font-size、color 等等,非常方便。並且字型圖示是向量圖,不會失真。還有一個優點是生成的檔案特別小。

壓縮字型檔案

使用 fontmin-webpack 外掛對字型檔案進行壓縮(感謝前端小偉提供)。

前端效能最佳化 24 條建議(2020)

參考資料:

fontmin-webpack

Iconfont-阿里巴巴向量圖示庫

7。 善用快取,不重複載入相同的資源

為了避免使用者每次訪問網站都得請求檔案,我們可以透過新增 Expires 或 max-age 來控制這一行為。Expires 設定了一個時間,只要在這個時間之前,瀏覽器都不會請求檔案,而是直接使用快取。而 max-age 是一個相對時間,建議使用 max-age 代替 Expires 。

不過這樣會產生一個問題,當檔案更新了怎麼辦?怎麼通知瀏覽器重新請求檔案?

可以透過更新頁面中引用的資源連結地址,讓瀏覽器主動放棄快取,載入新資源。

具體做法是把資源地址 URL 的修改與檔案內容關聯起來,也就是說,只有檔案內容變化,才會導致相應 URL 的變更,從而實現檔案級別的精確快取控制。什麼東西與檔案內容相關呢?我們會很自然的聯想到利用資料摘要要演算法對檔案求摘要資訊,摘要資訊與檔案內容一一對應,就有了一種可以精確到單個檔案粒度的快取控制依據了。

參考資料:

webpack + express 實現檔案精確快取

webpack-快取

張雲龍——大公司裡怎樣開發和部署前端程式碼?

8。 壓縮檔案

壓縮檔案可以減少檔案下載時間,讓使用者體驗性更好。

得益於 webpack 和 node 的發展,現在壓縮檔案已經非常方便了。

在 webpack 可以使用如下外掛進行壓縮:

JavaScript:UglifyPlugin

CSS :MiniCssExtractPlugin

HTML:HtmlWebpackPlugin

其實,我們還可以做得更好。那就是使用 gzip 壓縮。可以透過向 HTTP 請求頭中的 Accept-Encoding 頭新增 gzip 標識來開啟這一功能。當然,伺服器也得支援這一功能。

gzip 是目前最流行和最有效的壓縮方法。舉個例子,我用 Vue 開發的專案構建後生成的 app。js 檔案大小為 1。4MB,使用 gzip 壓縮後只有 573KB,體積減少了將近 60%。

附上 webpack 和 node 配置 gzip 的使用方法。

下載外掛

npm install compression-webpack-plugin ——save-devnpm install compression

webpack 配置

const CompressionPlugin = require(‘compression-webpack-plugin’);module。exports = { plugins: [new CompressionPlugin()],}

node 配置

const compression = require(‘compression’)// 在其他中介軟體前使用app。use(compression())

9。 圖片最佳化

(1)。 圖片延遲載入

在頁面中,先不給圖片設定路徑,只有當圖片出現在瀏覽器的可視區域時,才去載入真正的圖片,這就是延遲載入。對於圖片很多的網站來說,一次性載入全部圖片,會對使用者體驗造成很大的影響,所以需要使用圖片延遲載入。

首先可以將圖片這樣設定,在頁面不可見時圖片不會載入:

等頁面可見時,使用 JS 載入圖片:

const img = document。querySelector(‘img’)img。src = img。dataset。src

這樣圖片就加載出來了,完整的程式碼可以看一下參考資料。

參考資料:

web 前端圖片懶載入實現原理

(2)。 響應式圖片

響應式圖片的優點是瀏覽器能夠根據螢幕大小自動載入合適的圖片。

透過

picture

實現

前端效能最佳化 24 條建議(2020)

透過

@media

實現

@media (min-width: 769px) { 。bg { background-image: url(bg1080。jpg); }}@media (max-width: 768px) { 。bg { background-image: url(bg768。jpg); }}

(3)。 調整圖片大小

例如,你有一個 1920 * 1080 大小的圖片,用縮圖的方式展示給使用者,並且當用戶滑鼠懸停在上面時才展示全圖。如果使用者從未真正將滑鼠懸停在縮圖上,則浪費了下載圖片的時間。

所以,我們可以用兩張圖片來實行最佳化。一開始,只加載縮圖,當用戶懸停在圖片上時,才載入大圖。還有一種辦法,即對大圖進行延遲載入,在所有元素都載入完成後手動更改大圖的 src 進行下載。

(4)。 降低圖片質量

例如 JPG 格式的圖片,100% 的質量和 90% 質量的通常看不出來區別,尤其是用來當背景圖的時候。我經常用 PS 切背景圖時, 將圖片切成 JPG 格式,並且將它壓縮到 60% 的質量,基本上看不出來區別。

壓縮方法有兩種,一是透過 webpack 外掛

image-webpack-loader

,二是透過線上網站進行壓縮。

以下附上 webpack 外掛

image-webpack-loader

的用法。

npm i -D image-webpack-loader

webpack 配置

{ test: /\。(png|jpe?g|gif|svg)(\?。*)?$/, use:[ { loader: ‘url-loader’, options: { limit: 10000, /* 圖片大小小於1000位元組限制時會自動轉成 base64 碼引用*/ name: utils。assetsPath(‘img/[name]。[hash:7]。[ext]’) } }, /*對圖片進行壓縮*/ { loader: ‘image-webpack-loader’, options: { bypassOnDebug: true, } } ]}

(5)。 儘可能利用 CSS3 效果代替圖片

有很多圖片使用 CSS 效果(漸變、陰影等)就能畫出來,這種情況選擇 CSS3 效果更好。因為程式碼大小通常是圖片大小的幾分之一甚至幾十分之一。

參考資料:

img圖片在webpack中使用

(6)。 使用 webp 格式的圖片

WebP 的優勢體現在它具有更優的影象資料壓縮演算法,能帶來更小的圖片體積,而且擁有肉眼識別無差異的影象質量;同時具備了無損和有損的壓縮模式、Alpha 透明以及動畫的特性,在 JPEG 和 PNG 上的轉化效果都相當優秀、穩定和統一。

參考資料:

WebP 相對於 PNG、JPG 有什麼優勢?

10。 透過 webpack 按需載入程式碼,提取第三庫程式碼,減少 ES6 轉為 ES5 的冗餘程式碼

懶載入或者按需載入,是一種很好的最佳化網頁或應用的方式。這種方式實際上是先把你的程式碼在一些邏輯斷點處分離開,然後在一些程式碼塊中完成某些操作後,立即引用或即將引用另外一些新的程式碼塊。這樣加快了應用的初始載入速度,減輕了它的總體體積,因為某些程式碼塊可能永遠不會被載入。

根據檔案內容生成檔名,結合 import 動態引入元件實現按需載入

透過配置 output 的 filename 屬性可以實現這個需求。filename 屬性的值選項中有一個 [contenthash],它將根據檔案內容創建出唯一 hash。當檔案內容發生變化時,[contenthash] 也會發生變化。

output: { filename: ‘[name]。[contenthash]。js’, chunkFilename: ‘[name]。[contenthash]。js’, path: path。resolve(__dirname, ‘。。/dist’),},

提取第三方庫

由於引入的第三方庫一般都比較穩定,不會經常改變。所以將它們單獨提取出來,作為長期快取是一個更好的選擇。 這裡需要使用 webpack4 的 splitChunk 外掛 cacheGroups 選項。

optimization: { runtimeChunk: { name: ‘manifest’ // 將 webpack 的 runtime 程式碼拆分為一個單獨的 chunk。 }, splitChunks: { cacheGroups: { vendor: { name: ‘chunk-vendors’, test: /[\\/]node_modules[\\/]/, priority: -10, chunks: ‘initial’ }, common: { name: ‘chunk-common’, minChunks: 2, priority: -20, chunks: ‘initial’, reuseExistingChunk: true } }, }},

test: 用於控制哪些模組被這個快取組匹配到。原封不動傳遞出去的話,它預設會選擇所有的模組。可以傳遞的值型別:RegExp、String和Function;

priority:表示抽取權重,數字越大表示優先順序越高。因為一個 module 可能會滿足多個 cacheGroups 的條件,那麼抽取到哪個就由權重最高的說了算;

reuseExistingChunk:表示是否使用已有的 chunk,如果為 true 則表示如果當前的 chunk 包含的模組已經被抽取出去了,那麼將不會重新生成新的。

minChunks(預設是1):在分割之前,這個程式碼塊最小應該被引用的次數(譯註:保證程式碼塊複用性,預設配置的策略是不需要多次引用也可以被分割)

chunks (預設是async) :initial、async和all

name(打包的chunks的名字):字串或者函式(函式可以根據條件自定義名字)

減少 ES6 轉為 ES5 的冗餘程式碼

Babel 轉化後的程式碼想要實現和原來程式碼一樣的功能需要藉助一些幫助函式,比如:

class Person {}

會被轉換為:

“use strict”;function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Person = function Person() { _classCallCheck(this, Person);};

這裡

_classCallCheck

就是一個

helper

函式,如果在很多檔案裡都聲明瞭類,那麼就會產生很多個這樣的

helper

函式。

這裡的

@babel/runtime

包就聲明瞭所有需要用到的幫助函式,而

@babel/plugin-transform-runtime

的作用就是將所有需要

helper

函式的檔案,從

@babel/runtime包

引進來:

“use strict”;var _classCallCheck2 = require(“@babel/runtime/helpers/classCallCheck”);var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);function _interopRequireDefault(obj) { return obj && obj。__esModule ? obj : { default: obj };}var Person = function Person() { (0, _classCallCheck3。default)(this, Person);};

這裡就沒有再編譯出

helper

函式

classCallCheck

了,而是直接引用了

@babel/runtime

中的

helpers/classCallCheck

安裝

npm i -D @babel/plugin-transform-runtime @babel/runtime

使用

。babelrc

檔案中

“plugins”: [ “@babel/plugin-transform-runtime”]

參考資料:

Babel 7。1介紹 transform-runtime polyfill env

懶載入

Vue 路由懶載入

webpack 快取

一步一步的瞭解webpack4的splitChunk外掛

11。 減少重繪重排

瀏覽器渲染過程

解析HTML生成DOM樹。

解析CSS生成CSSOM規則樹。

將DOM樹與CSSOM規則樹合併在一起生成渲染樹。

遍歷渲染樹開始佈局,計算每個節點的位置大小資訊。

將渲染樹每個節點繪製到螢幕。

前端效能最佳化 24 條建議(2020)

重排

當改變 DOM 元素位置或大小時,會導致瀏覽器重新生成渲染樹,這個過程叫重排。

重繪

當重新生成渲染樹後,就要將渲染樹每個節點繪製到螢幕,這個過程叫重繪。不是所有的動作都會導致重排,例如改變字型顏色,只會導致重繪。記住,重排會導致重繪,重繪不會導致重排 。

重排和重繪這兩個操作都是非常昂貴的,因為 JavaScript 引擎執行緒與 GUI 渲染執行緒是互斥,它們同時只能一個在工作。

什麼操作會導致重排?

新增或刪除可見的 DOM 元素

元素位置改變

元素尺寸改變

內容改變

瀏覽器視窗尺寸改變

如何減少重排重繪?

用 JavaScript 修改樣式時,最好不要直接寫樣式,而是替換 class 來改變樣式。

如果要對 DOM 元素執行一系列操作,可以將 DOM 元素脫離文件流,修改完成後,再將它帶回文件。推薦使用隱藏元素(display:none)或文件碎片(DocumentFragement),都能很好的實現這個方案。

12。 使用事件委託

事件委託利用了事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。所有用到按鈕的事件(多數滑鼠事件和鍵盤事件)都適合採用事件委託技術, 使用事件委託可以節省記憶體。

  • 蘋果
  • 香蕉
  • 鳳梨
// gooddocument。querySelector(‘ul’)。onclick = (event) => { const target = event。target if (target。nodeName === ‘LI’) { console。log(target。innerHTML) }}// baddocument。querySelectorAll(‘li’)。forEach((e) => { e。onclick = function() { console。log(this。innerHTML) }})

13。 注意程式的區域性性

一個編寫良好的計算機程式常常具有良好的區域性性,它們傾向於引用最近引用過的資料項附近的資料項,或者最近引用過的資料項本身,這種傾向性,被稱為區域性性原理。有良好區域性性的程式比區域性性差的程式執行得更快。

區域性性通常有兩種不同的形式:

時間區域性性:在一個具有良好時間區域性性的程式中,被引用過一次的記憶體位置很可能在不遠的將來被多次引用。

空間區域性性 :在一個具有良好空間區域性性的程式中,如果一個記憶體位置被引用了一次,那麼程式很可能在不遠的將來引用附近的一個記憶體位置。

時間區域性性示例

function sum(arry) { let i, sum = 0 let len = arry。length for (i = 0; i < len; i++) { sum += arry[i] } return sum}

在這個例子中,變數sum在每次迴圈迭代中被引用一次,因此,對於sum來說,具有良好的時間區域性性

空間區域性性示例

具有良好空間區域性性的程式

// 二維陣列 function sum1(arry, rows, cols) { let i, j, sum = 0 for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { sum += arry[i][j] } } return sum}

空間區域性性差的程式

// 二維陣列 function sum2(arry, rows, cols) { let i, j, sum = 0 for (j = 0; j < cols; j++) { for (i = 0; i < rows; i++) { sum += arry[i][j] } } return sum}

看一下上面的兩個空間區域性性示例,像示例中從每行開始按順序訪問陣列每個元素的方式,稱為具有步長為1的引用模式。 如果在陣列中,每隔k個元素進行訪問,就稱為步長為k的引用模式。 一般而言,隨著步長的增加,空間區域性性下降。

這兩個例子有什麼區別?區別在於第一個示例是按行掃描陣列,每掃描完一行再去掃下一行;第二個示例是按列來掃描陣列,掃完一行中的一個元素,馬上就去掃下一行中的同一列元素。

陣列在記憶體中是按照行順序來存放的,結果就是逐行掃描陣列的示例得到了步長為 1 引用模式,具有良好的空間區域性性;而另一個示例步長為 rows,空間區域性性極差。

效能測試

執行環境:

cpu: i5-7400

瀏覽器: chrome 70。0。3538。110

對一個長度為9000的二維陣列(子陣列長度也為9000)進行10次空間區域性性測試,時間(毫秒)取平均值,結果如下:

所用示例為上述兩個空間區域性性示例

前端效能最佳化 24 條建議(2020)

從以上測試結果來看,步長為 1 的陣列執行時間比步長為 9000 的陣列快了一個數量級。

總結:

重複引用相同變數的程式具有良好的時間區域性性

對於具有步長為 k 的引用模式的程式,步長越小,空間區域性性越好;而在記憶體中以大步長跳來跳去的程式空間區域性性會很差

參考資料:

深入理解計算機系統

14。 if-else 對比 switch

當判斷條件數量越來越多時,越傾向於使用 switch 而不是 if-else。

if (color == ‘blue’) {} else if (color == ‘yellow’) {} else if (color == ‘white’) {} else if (color == ‘black’) {} else if (color == ‘green’) {} else if (color == ‘orange’) {} else if (color == ‘pink’) {}switch (color) { case ‘blue’: break case ‘yellow’: break case ‘white’: break case ‘black’: break case ‘green’: break case ‘orange’: break case ‘pink’: break}

像以上這種情況,使用 switch 是最好的。假設 color 的值為 pink,則 if-else 語句要進行 7 次判斷,switch 只需要進行一次判斷。 從可讀性來說,switch 語句也更好。

從使用時機來說,當條件值大於兩個的時候,使用 switch 更好。不過 if-else 也有 switch 無法做到的事情,例如有多個判斷條件的情況下,無法使用 switch。

15。 查詢表

當條件語句特別多時,使用 switch 和 if-else 不是最佳的選擇,這時不妨試一下查詢表。查詢表可以使用陣列和物件來構建。

switch (index) { case ‘0’: return result0 case ‘1’: return result1 case ‘2’: return result2 case ‘3’: return result3 case ‘4’: return result4 case ‘5’: return result5 case ‘6’: return result6 case ‘7’: return result7 case ‘8’: return result8 case ‘9’: return result9 case ‘10’: return result10 case ‘11’: return result11}

可以將這個 switch 語句轉換為查詢表

const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]return results[index]

如果條件語句不是數值而是字串,可以用物件來建立查詢表

const map = { red: result0, green: result1,}return map[color]

16。 避免頁面卡頓

60fps 與裝置重新整理率

目前大多數裝置的螢幕重新整理率為 60 次/秒。因此,如果在頁面中有一個動畫或漸變效果,或者使用者正在滾動頁面,那麼瀏覽器渲染動畫或頁面的每一幀的速率也需要跟裝置螢幕的重新整理率保持一致。 其中每個幀的預算時間僅比 16 毫秒多一點 (1 秒/ 60 = 16。66 毫秒)。但實際上,瀏覽器有整理工作要做,因此您的所有工作需要在 10 毫秒內完成。如果無法符合此預算,幀率將下降,並且內容會在螢幕上抖動。 此現象通常稱為卡頓,會對使用者體驗產生負面影響。

前端效能最佳化 24 條建議(2020)

假如你用 JavaScript 修改了 DOM,並觸發樣式修改,經歷重排重繪最後畫到螢幕上。如果這其中任意一項的執行時間過長,都會導致渲染這一幀的時間過長,平均幀率就會下降。假設這一幀花了 50 ms,那麼此時的幀率為 1s / 50ms = 20fps,頁面看起來就像卡頓了一樣。

對於一些長時間執行的 JavaScript,我們可以使用定時器進行切分,延遲執行。

for (let i = 0, len = arry。length; i < len; i++) { process(arry[i])}

假設上面的迴圈結構由於 process() 複雜度過高或陣列元素太多,甚至兩者都有,可以嘗試一下切分。

const todo = arry。concat()setTimeout(function(){ process(todo。shift()) if (todo。length) { setTimeout(arguments。callee, 25) } else { callback(arry) }}, 25)

如果有興趣瞭解更多,可以檢視一下高效能JavaScript第 6 章和高效前端:Web高效程式設計與最佳化實踐第 3 章。

參考資料:

渲染效能

17。 使用 requestAnimationFrame 來實現視覺變化

從第 16 點我們可以知道,大多數裝置螢幕重新整理率為 60 次/秒,也就是說每一幀的平均時間為 16。66 毫秒。在使用 JavaScript 實現動畫效果的時候,最好的情況就是每次程式碼都是在幀的開頭開始執行。而保證 JavaScript 在幀開始時執行的唯一方式是使用

requestAnimationFrame

/** * If run as a requestAnimationFrame callback, this * will be run at the start of the frame。 */function updateScreen(time) { // Make visual updates here。}requestAnimationFrame(updateScreen);

如果採取

setTimeout

setInterval

來實現動畫的話,回撥函式將在幀中的某個時點執行,可能剛好在末尾,而這可能經常會使我們丟失幀,導致卡頓。

前端效能最佳化 24 條建議(2020)

參考資料:

最佳化 JavaScript 執行

18。 使用 Web Workers

Web Worker 使用其他工作執行緒從而獨立於主執行緒之外,它可以執行任務而不干擾使用者介面。一個 worker 可以將訊息傳送到建立它的 JavaScript 程式碼, 透過將訊息傳送到該程式碼指定的事件處理程式(反之亦然)。

Web Worker 適用於那些處理純資料,或者與瀏覽器 UI 無關的長時間執行指令碼。

建立一個新的 worker 很簡單,指定一個指令碼的 URI 來執行 worker 執行緒(main。js):

var myWorker = new Worker(‘worker。js’);// 你可以透過postMessage() 方法和onmessage事件向worker傳送訊息。first。onchange = function() { myWorker。postMessage([first。value,second。value]); console。log(‘Message posted to worker’);}second。onchange = function() { myWorker。postMessage([first。value,second。value]); console。log(‘Message posted to worker’);}

在 worker 中接收到訊息後,我們可以寫一個事件處理函式程式碼作為響應(worker。js):

onmessage = function(e) { console。log(‘Message received from main script’); var workerResult = ‘Result: ’ + (e。data[0] * e。data[1]); console。log(‘Posting message back to main script’); postMessage(workerResult);}

onmessage處理函式在接收到訊息後馬上執行,程式碼中訊息本身作為事件的data屬性進行使用。這裡我們簡單的對這2個數字作乘法處理並再次使用postMessage()方法,將結果回傳給主執行緒。

回到主執行緒,我們再次使用onmessage以響應worker回傳的訊息:

myWorker。onmessage = function(e) { result。textContent = e。data; console。log(‘Message received from worker’);}

在這裡我們獲取訊息事件的data,並且將它設定為result的textContent,所以使用者可以直接看到運算的結果。

不過在worker內,不能直接操作DOM節點,也不能使用window物件的預設方法和屬性。然而你可以使用大量window物件之下的東西,包括WebSockets,IndexedDB以及FireFox OS專用的Data Store API等資料儲存機制。

參考資料:

Web Workers

19。 使用位操作

JavaScript 中的數字都使用 IEEE-754 標準以 64 位格式儲存。但是在位操作中,數字被轉換為有符號的 32 位格式。即使需要轉換,位操作也比其他數學運算和布林操作快得多。

取模

由於偶數的最低位為 0,奇數為 1,所以取模運算可以用位操作來代替。

if (value % 2) { // 奇數} else { // 偶數 }// 位操作if (value & 1) { // 奇數} else { // 偶數}

取整

~~10。12 // 10~~10 // 10~~‘1。5’ // 1~~undefined // 0~~null // 0

位掩碼

const a = 1const b = 2const c = 4const options = a | b | c

透過定義這些選項,可以用按位與操作來判斷 a/b/c 是否在 options 中。

// 選項 b 是否在選項中if (b & options) { 。。。}

20。 不要覆蓋原生方法

無論你的 JavaScript 程式碼如何最佳化,都比不上原生方法。因為原生方法是用低階語言寫的(C/C++),並且被編譯成機器碼,成為瀏覽器的一部分。當原生方法可用時,儘量使用它們,特別是數學運算和 DOM 操作。

21。 降低 CSS 選擇器的複雜性

(1)。 瀏覽器讀取選擇器,遵循的原則是從選擇器的右邊到左邊讀取。

看個示例

#block 。text p { color: red;}

查詢所有 P 元素。

查詢結果 1 中的元素是否有類名為 text 的父元素

查詢結果 2 中的元素是否有 id 為 block 的父元素

(2)。 CSS 選擇器優先順序

內聯 > ID選擇器 > 類選擇器 > 標籤選擇器

根據以上兩個資訊可以得出結論。

選擇器越短越好。

儘量使用高優先順序的選擇器,例如 ID 和類選擇器。

避免使用萬用字元 *。

最後要說一句,據我查詢的資料所得,CSS 選擇器沒有最佳化的必要,因為最慢和慢快的選擇器效能差別非常小。

參考資料:

CSS selector performance

Optimizing CSS: ID Selectors and Other Myths

22。 使用 flexbox 而不是較早的佈局模型

在早期的 CSS 佈局方式中我們能對元素實行絕對定位、相對定位或浮動定位。而現在,我們有了新的佈局方式 flexbox,它比起早期的佈局方式來說有個優勢,那就是效能比較好。

下面的截圖顯示了在 1300 個框上使用浮動的佈局開銷:

前端效能最佳化 24 條建議(2020)

然後我們用 flexbox 來重現這個例子:

前端效能最佳化 24 條建議(2020)

現在,對於相同數量的元素和相同的視覺外觀,佈局的時間要少得多(本例中為分別 3。5 毫秒和 14 毫秒)。

不過 flexbox 相容性還是有點問題,不是所有瀏覽器都支援它,所以要謹慎使用。

各瀏覽器相容性:

Chrome 29+

Firefox 28+

Internet Explorer 11

Opera 17+

Safari 6。1+ (prefixed with -webkit-)

Android 4。4+

iOS 7。1+ (prefixed with -webkit-)

參考資料:

使用 flexbox 而不是較早的佈局模型

23。 使用 transform 和 opacity 屬性更改來實現動畫

在 CSS 中,transforms 和 opacity 這兩個屬性更改不會觸發重排與重繪,它們是可以由合成器(composite)單獨處理的屬性。

前端效能最佳化 24 條建議(2020)

參考資料:

使用 transform 和 opacity 屬性更改來實現動畫

24。 合理使用規則,避免過度最佳化

效能最佳化主要分為兩類:

載入時最佳化

執行時最佳化

上述 23 條建議中,屬於載入時最佳化的是前面 10 條建議,屬於執行時最佳化的是後面 13 條建議。通常來說,沒有必要 23 條效能最佳化規則都用上,根據網站使用者群體來做針對性的調整是最好的,節省精力,節省時間。

在解決問題之前,得先找出問題,否則無從下手。所以在做效能最佳化之前,最好先調查一下網站的載入效能和執行效能。

檢查載入效能

一個網站載入效能如何主要看白屏時間和首屏時間。

白屏時間:指從輸入網址,到頁面開始顯示內容的時間。

首屏時間:指從輸入網址,到頁面完全渲染的時間。

將以下指令碼放在

前面就能獲取白屏時間。

window。onload

事件裡執行

new Date() - performance。timing。navigationStart

即可獲取首屏時間。

檢查執行效能

配合 chrome 的開發者工具,我們可以檢視網站在執行時的效能。

開啟網站,按 F12 選擇 performance,點選左上角的灰色圓點,變成紅色就代表開始記錄了。這時可以模仿使用者使用網站,在使用完畢後,點選 stop,然後你就能看到網站執行期間的效能報告。如果有紅色的塊,代表有掉幀的情況;如果是綠色,則代表 FPS 很好。performance 的具體使用方法請用搜索引擎搜尋一下,畢竟篇幅有限。

透過檢查載入和執行效能,相信你對網站效能已經有了大概瞭解。所以這時候要做的事情,就是使用上述 23 條建議盡情地去最佳化你的網站,加油!

參考資料:

performance。timing。navigationStart

其他參考資料

效能為何至關重要

高效能網站建設指南

Web效能權威指南

高效能JavaScript

高效前端:Web高效程式設計與最佳化實踐