前端性能優化 24 條建議(2020)

性能優化是把雙刃劍,有好的一面也有壞的一面。好的一面就是能提高網站性能,壞的一面就是配置麻煩,或者要遵照的規則太多。而且某些性能優化規則並不適用全部場景,須要謹慎使用,請讀者帶着批判性的眼光來閱讀本文。javascript

本文相關的優化建議的引用資料出處均會在建議後面給出,或者放在文末。css

1. 減小 HTTP 請求

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

在這裏插入圖片描述

這是一個 HTTP 請求,請求的文件大小爲 28.4KB。前端

名詞解釋:vue

  • 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 請求次數的緣由。java

參考資料:node

2. 使用 HTTP2

HTTP2 相比 HTTP1.1 有以下幾個優勢:webpack

解析速度快

服務器解析 HTTP1.1 的請求時,必須不斷地讀入字節,直到遇到分隔符 CRLF 爲止。而解析 HTTP2 的請求就不用這麼麻煩,由於 HTTP2 是基於幀的協議,每一個幀都有表示幀長度的字段。git

多路複用

HTTP1.1 若是要同時發起多個請求,就得創建多個 TCP 鏈接,由於一個 TCP 鏈接同時只能處理一個 HTTP1.1 的請求。github

在 HTTP2 上,多個請求能夠共用一個 TCP 鏈接,這稱爲多路複用。同一個請求和響應用一個流來表示,並有惟一的流 ID 來標識。
多個請求和響應在 TCP 鏈接中能夠亂序發送,到達目的地後再經過流 ID 從新組建。

首部壓縮

HTTP2 提供了首部壓縮功能。

例若有以下兩個請求:

:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-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:foo
Header2:bar
Header3:bat

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

索引 首部名稱
62 Header1 foo
63 Header2 bar
64 Header3 bat

若是服務器收到了請求,它會照樣建立一張表。
當客戶端發送下一個請求的時候,若是首部相同,它能夠直接發送這樣的首部塊:

62 63 64

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

優先級

HTTP2 能夠對比較緊急的請求設置一個較高的優先級,服務器在收到這樣的請求後,能夠優先處理。

流量控制

因爲一個 TCP 鏈接流量帶寬(根據客戶端到服務器的網絡帶寬而定)是固定的,當有多個請求併發時,一個請求佔的流量多,另外一個請求佔的流量就會少。流量控制能夠對不一樣的流的流量進行精確控制。

服務器推送

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

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

如今有不少網站已經開始使用 HTTP2 了,例如知乎:

在這裏插入圖片描述

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

參考資料:

3. 使用服務端渲染

客戶端渲染: 獲取 HTML 文件,根據須要下載 JavaScript 文件,運行文件,生成 DOM,再渲染。

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

  • 優勢:首屏渲染快,SEO 好。
  • 缺點:配置麻煩,增長了服務器的計算壓力。

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

客戶端渲染過程

  1. 訪問客戶端渲染的網站。
  2. 服務器返回一個包含了引入資源語句和 <div id="app"></div> 的 HTML 文件。
  3. 客戶端經過 HTTP 向服務器請求資源,當必要的資源都加載完畢後,執行 new Vue() 開始實例化並渲染頁面。

服務端渲染過程

  1. 訪問服務端渲染的網站。
  2. 服務器會查看當前路由組件須要哪些資源文件,而後將這些文件的內容填充到 HTML 文件。若是有 ajax 請求,就會執行它進行數據預取並填充到 HTML 文件裏,最後返回這個 HTML 頁面。
  3. 當客戶端接收到這個 HTML 頁面時,能夠立刻就開始渲染頁面。與此同時,頁面也會加載資源,當必要的資源都加載完畢後,開始執行 new Vue() 開始實例化並接管頁面。

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

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

假設你的網站須要加載完 abcd 四個文件才能渲染完畢。而且每一個文件大小爲 1 M。

這樣一算:客戶端渲染的網站須要加載 4 個文件和 HTML 文件才能完成首頁渲染,總計大小爲 4M(忽略 HTML 文件大小)。而服務端渲染的網站只須要加載一個渲染完畢的 HTML 文件就能完成首頁渲染,總計大小爲已經渲染完畢的 HTML 文件(這種文件不會太大,通常爲幾百K,個人我的博客網站(SSR)加載的 HTML 文件爲 400K)。這就是服務端渲染更快的緣由

參考資料:

4. 靜態資源使用 CDN

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

CDN 原理

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

  1. 瀏覽器要將域名解析爲 IP 地址,因此須要向本地 DNS 發出請求。
  2. 本地 DNS 依次向根服務器、頂級域名服務器、權限服務器發出請求,獲得網站服務器的 IP 地址。
  3. 本地 DNS 將 IP 地址發回給瀏覽器,瀏覽器向網站服務器 IP 地址發出請求並獲得資源。

若是用戶訪問的網站部署了 CDN,過程是這樣的:

  1. 瀏覽器要將域名解析爲 IP 地址,因此須要向本地 DNS 發出請求。
  2. 本地 DNS 依次向根服務器、頂級域名服務器、權限服務器發出請求,獲得全局負載均衡系統(GSLB)的 IP 地址。
  3. 本地 DNS 再向 GSLB 發出請求,GSLB 的主要功能是根據本地 DNS 的 IP 地址判斷用戶的位置,篩選出距離用戶較近的本地負載均衡系統(SLB),並將該 SLB 的 IP 地址做爲結果返回給本地 DNS。
  4. 本地 DNS 將 SLB 的 IP 地址發回給瀏覽器,瀏覽器向 SLB 發出請求。
  5. SLB 根據瀏覽器請求的資源和地址,選出最優的緩存服務器發回給瀏覽器。
  6. 瀏覽器再根據 SLB 發回的地址重定向到緩存服務器。
  7. 若是緩存服務器有瀏覽器須要的資源,就將資源發回給瀏覽器。若是沒有,就向源服務器請求資源,再發給瀏覽器並緩存在本地。

參考資料:

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 插件對字體文件進行壓縮(感謝前端小偉提供)。

參考資料:

7. 善用緩存,不重複加載相同的資源

爲了不用戶每次訪問網站都得請求文件,咱們能夠經過添加 Expires 或 max-age 來控制這一行爲。Expires 設置了一個時間,只要在這個時間以前,瀏覽器都不會請求文件,而是直接使用緩存。而 max-age 是一個相對時間,建議使用 max-age 代替 Expires 。

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

能夠經過更新頁面中引用的資源連接地址,讓瀏覽器主動放棄緩存,加載新資源。

具體作法是把資源地址 URL 的修改與文件內容關聯起來,也就是說,只有文件內容變化,纔會致使相應 URL 的變動,從而實現文件級別的精確緩存控制。什麼東西與文件內容相關呢?咱們會很天然的聯想到利用數據摘要要算法對文件求摘要信息,摘要信息與文件內容一一對應,就有了一種能夠精確到單個文件粒度的緩存控制依據了。

參考資料:

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-dev
npm install compression

webpack 配置

const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [new CompressionPlugin()],
}

node 配置

const compression = require('compression')
// 在其餘中間件前使用
app.use(compression())

9. 圖片優化

(1). 圖片延遲加載

在頁面中,先不給圖片設置路徑,只有當圖片出如今瀏覽器的可視區域時,纔去加載真正的圖片,這就是延遲加載。對於圖片不少的網站來講,一次性加載所有圖片,會對用戶體驗形成很大的影響,因此須要使用圖片延遲加載。

首先能夠將圖片這樣設置,在頁面不可見時圖片不會加載:

<img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">

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

const img = document.querySelector('img')
img.src = img.dataset.src

這樣圖片就加載出來了,完整的代碼能夠看一下參考資料。

參考資料:

(2). 響應式圖片

響應式圖片的優勢是瀏覽器可以根據屏幕大小自動加載合適的圖片。

經過 picture 實現

<picture>
    <source srcset="banner_w1000.jpg" media="(min-width: 801px)">
    <source srcset="banner_w800.jpg" media="(max-width: 800px)">
    <img src="banner_w800.jpg" alt="">
</picture>

經過 @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 效果更好。由於代碼大小一般是圖片大小的幾分之一甚至幾十分之一。

參考資料:

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

WebP 的優點體如今它具備更優的圖像數據壓縮算法,能帶來更小的圖片體積,並且擁有肉眼識別無差別的圖像質量;同時具有了無損和有損的壓縮模式、Alpha 透明以及動畫的特性,在 JPEG 和 PNG 上的轉化效果都至關優秀、穩定和統一。

參考資料:

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"
]

參考資料:

11. 減小重繪重排

瀏覽器渲染過程

  1. 解析HTML生成DOM樹。
  2. 解析CSS生成CSSOM規則樹。
  3. 將DOM樹與CSSOM規則樹合併在一塊兒生成渲染樹。
  4. 遍歷渲染樹開始佈局,計算每一個節點的位置大小信息。
  5. 將渲染樹每一個節點繪製到屏幕。

在這裏插入圖片描述

重排

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

重繪

當從新生成渲染樹後,就要將渲染樹每一個節點繪製到屏幕,這個過程叫重繪。不是全部的動做都會致使重排,例如改變字體顏色,只會致使重繪。記住,重排會致使重繪,重繪不會致使重排 。

重排和重繪這兩個操做都是很是昂貴的,由於 JavaScript 引擎線程與 GUI 渲染線程是互斥,它們同時只能一個在工做。

什麼操做會致使重排?

  • 添加或刪除可見的 DOM 元素
  • 元素位置改變
  • 元素尺寸改變
  • 內容改變
  • 瀏覽器窗口尺寸改變

如何減小重排重繪?

  • 用 JavaScript 修改樣式時,最好不要直接寫樣式,而是替換 class 來改變樣式。
  • 若是要對 DOM 元素執行一系列操做,能夠將 DOM 元素脫離文檔流,修改完成後,再將它帶回文檔。推薦使用隱藏元素(display:none)或文檔碎片(DocumentFragement),都能很好的實現這個方案。

12. 使用事件委託

事件委託利用了事件冒泡,只指定一個事件處理程序,就能夠管理某一類型的全部事件。全部用到按鈕的事件(多數鼠標事件和鍵盤事件)都適合採用事件委託技術, 使用事件委託能夠節省內存。

<ul>
  <li>蘋果</li>
  <li>香蕉</li>
  <li>鳳梨</li>
</ul>

// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.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次空間局部性測試,時間(毫秒)取平均值,結果以下:

所用示例爲上述兩個空間局部性示例

步長爲 1 步長爲 9000
124 2316

從以上測試結果來看,步長爲 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 毫秒內完成。若是沒法符合此預算,幀率將降低,而且內容會在屏幕上抖動。 此現象一般稱爲卡頓,會對用戶體驗產生負面影響。

在這裏插入圖片描述

假如你用 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);

若是採起 setTimeoutsetInterval 來實現動畫的話,回調函數將在幀中的某個時點運行,可能恰好在末尾,而這可能常常會使咱們丟失幀,致使卡頓。

在這裏插入圖片描述

參考資料:

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等數據存儲機制。

參考資料:

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 = 1
const b = 2
const c = 4
const 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;
}
  1. 查找全部 P 元素。
  2. 查找結果 1 中的元素是否有類名爲 text 的父元素
  3. 查找結果 2 中的元素是否有 id 爲 block 的父元素

(2). CSS 選擇器優先級

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

根據以上兩個信息能夠得出結論。

  1. 選擇器越短越好。
  2. 儘可能使用高優先級的選擇器,例如 ID 和類選擇器。
  3. 避免使用通配符 *。

最後要說一句,據我查找的資料所得,CSS 選擇器沒有優化的必要,由於最慢和慢快的選擇器性能差異很是小。

參考資料:

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

在早期的 CSS 佈局方式中咱們能對元素實行絕對定位、相對定位或浮動定位。而如今,咱們有了新的佈局方式 flexbox,它比起早期的佈局方式來講有個優點,那就是性能比較好。

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

在這裏插入圖片描述

而後咱們用 flexbox 來重現這個例子:

在這裏插入圖片描述

如今,對於相同數量的元素和相同的視覺外觀,佈局的時間要少得多(本例中爲分別 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-)

參考資料:

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

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

在這裏插入圖片描述

參考資料:

24. 合理使用規則,避免過分優化

性能優化主要分爲兩類:

  1. 加載時優化
  2. 運行時優化

上述 23 條建議中,屬於加載時優化的是前面 10 條建議,屬於運行時優化的是後面 13 條建議。一般來講,沒有必要 23 條性能優化規則都用上,根據網站用戶羣體來作針對性的調整是最好的,節省精力,節省時間。

在解決問題以前,得先找出問題,不然無從下手。因此在作性能優化以前,最好先調查一下網站的加載性能和運行性能。

檢查加載性能

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

  • 白屏時間:指從輸入網址,到頁面開始顯示內容的時間。
  • 首屏時間:指從輸入網址,到頁面徹底渲染的時間。

將如下腳本放在 </head> 前面就能獲取白屏時間。

<script>
    new Date() - performance.timing.navigationStart
</script>

window.onload 事件裏執行 new Date() - performance.timing.navigationStart 便可獲取首屏時間。

檢查運行性能

配合 chrome 的開發者工具,咱們能夠查看網站在運行時的性能。

打開網站,按 F12 選擇 performance,點擊左上角的灰色圓點,變成紅色就表明開始記錄了。這時能夠模仿用戶使用網站,在使用完畢後,點擊 stop,而後你就能看到網站運行期間的性能報告。若是有紅色的塊,表明有掉幀的狀況;若是是綠色,則表明 FPS 很好。performance 的具體使用方法請用搜索引擎搜索一下,畢竟篇幅有限。

經過檢查加載和運行性能,相信你對網站性能已經有了大概瞭解。因此這時候要作的事情,就是使用上述 23 條建議盡情地去優化你的網站,加油!

參考資料:

其餘參考資料

更多文章,歡迎關注

本文同步分享在 博客「譚光志」(SegmentFault)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索