阿里前端大牛:性能優化12條建議

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

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

1. 減小 HTTP 請求

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

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

名詞解釋:webpack

  • Queueing: 在請求隊列中的時間。
  • Stalled: 從TCP 鏈接創建完成,到真正能夠傳輸數據之間的時間差,此時間包括代理協商時間。
  • Proxy negotiation: 與代理服務器鏈接進行協商所花費的時間。
  • DNS Lookup: 執行DNS查找所花費的時間,頁面上的每一個不一樣的域都須要進行DNS查找。
  • Initial Connection / Connecting: 創建鏈接所花費的時間,包括TCP握手/重試和協商SSL。
  • SSL: 完成SSL握手所花費的時間。
  • Request sent: 發出網絡請求所花費的時間,一般爲一毫秒的時間。
  • Waiting(TFFB): TFFB 是發出頁面請求到接收到應答數據第一個字節的時間總和,它包含了 DNS 解析時間、 TCP 鏈接時間、發送 HTTP 請求時間和得到響應消息第一個字節的時間。
  • Content Download: 接收響應數據所花費的時間。

從這個例子能夠看出,真正下載數據的時間佔比爲 13.05 / 204.16 = 6.39%,文件越小,這個比例越小,文件越大,比例就越高。這就是爲何要建議將多個小文件合併爲一個大文件,從而減小 HTTP 請求次數的緣由。git

參考資料:github

2. 使用 HTTP2

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

解析速度快

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

多路複用

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

在 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 好。
  • 缺點:配置麻煩,增長了服務器的計算壓力。

參考資料:

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)
  }
})

同時,我還從這位阿里大神手裏薅到一份阿里內部資料。

有須要的點擊這裏免費領取資料PDF

篇幅有限,僅展現部份內容

若是你須要這份完整版資料pdf,【點擊我】就能夠了。

但願你們明年的金三銀四面試順利,拿下本身心儀的offer!

相關文章
相關標籤/搜索