一次網站的性能優化之路 — 天下武功,唯快不破

  • 2019 年 10 月 3 日
  • 筆記

首屏作為直面用戶的第一屏,其重要性不言而喻,如何加快載入的速度是非常重要的一課。

本文講解的是:筆者對自己搭建的個人部落格網站的速度優化的經歷。

效果體驗地址:http://biaochenxuying.cn

1. 用戶期待的速度體驗

2018 年 8 月,百度搜索資源平台發布的《百度移動搜索落地頁體驗白皮書 4.0 》中提到:頁面的首屏內容應在 1.5 秒內載入完成

也許有人有疑惑:為什麼是 1.5 秒內?哪些方式可加快載入速度?以下將為您解答這些疑問!

移動互聯網時代,用戶對於網頁的打開速度要求越來越高。百度用戶體驗部研究表明,頁面放棄率和頁面的打開時間關係如下圖所示:

頁面放棄率和頁面的打開時間關係

根據百度用戶體驗部的研究結果來看,普通用戶期望且能夠接受的頁面載入時間在 3 秒以內。若頁面的載入時間過慢,用戶就會失去耐心而選擇離開,這對用戶和站長來說都是一大損失。

百度搜索資源平台有 「閃電演算法」 的支援,為了能夠保障用戶體驗,給予優秀站點更多面向用戶的機會,「閃電演算法」在 2017 年 10 月初上線。

閃電演算法 的具體內容如下:

移動網頁首屏在 2 秒之內完成打開的,在移動搜索下將獲得提升頁面評價優待,獲得流量傾斜;同時,在移動搜索頁面首屏載入非常慢(3 秒及以上)的網頁將會被打壓。

2. 分析問題

未優化之前,首屏時間居然大概要 7 – 10 秒,簡直不要太鬧心。

開始分析問題,先來看下 network :

主要問題:

  • 第一個文章列表介面用了 4.42 秒
  • 其他的後端介面速度也不快
  • 另外 js css 等靜態的文件也很大,請求的時間也很長

我還用了 Lighthouse 來測試和分析我的網站。

Lighthouse 是一個開源的自動化工具,用於改進網路應用的品質。 你可以將其作為一個 Chrome 擴展程式運行,或從命令行運行。 為 Lighthouse 提供一個需要審查的網址,它將針對此頁面運行一連串的測試,然後生成一個有關頁面性能的報告。

未優化之前:

image.png

上欄內容分別是頁面性能、PWA(漸進式 Web 應用)、可訪問性(無障礙)、最佳實踐、SEO 五項指標的跑分。

下欄是每一個指標的細化性能評估。

再看下 Lighthouse 對性能問題給出了可行的建議、以及每一項優化操作預期會幫我們節省的時間:

image.png

從上面可以看出,主要問題:

  • 圖片太大
  • 一開始圖片就載入了太多

知道問題所在就已經成功了一半了,接下來便開始優化之路。

2. 優化之路

網頁速度優化的方法實在太多,本文只說本次優化用到的方法。

2.1 前端優化

本項目前端部分是用了 react 和 antd,但是 webpack 用的還是 3.8.X 。

2.1.1 webpack 打包優化

因為 webpack4 對打包做了很多優化,比如 Tree-Shaking ,所以我用最新的 react-create-app 重構了一次項目,把項目升級了一遍,所有的依賴包都是目前最新的穩定版了,webpack 也升級到了 4.28.3 。

用最新 react-create-app 創建的項目,很多配置已經是很好了的,筆者只修改了兩處地方。

    1. 打包配置修改了 webpack.config.js 的這一行程式碼:
// Source maps are resource heavy and can cause out of memory issue for large source files.  const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';    // 把上面的程式碼修改為:  const shouldUseSourceMap = process.env.NODE_ENV === 'production' ? false : true;

生產環境下,打包去掉 SourceMap,靜態文件就很小了,從 13M 變成了 3M 。

    1. 還修改了圖片打包大小的限制,這樣子小於 40K 的圖片都會變成 base64 的圖片格式。
{        test: [/.bmp$/, /.gif$/, /.jpe?g$/, /.png$/,/.jpg$/,/.svg$/],        loader: require.resolve('url-loader'),        options: {              limit: 40000, // 把默認的 10000 修改為 40000              name: 'static/media/[name].[hash:8].[ext]',        },   }

2.1.2 去掉沒用的文件

比如之前可能覺得會有用的文件,後面發現用不到了,注釋或者刪除,比如 reducers 裡面的 home 模組。

import { combineReducers } from 'redux'  import { connectRouter } from 'connected-react-router'  // import { home } from './module/home'  import { user } from './module/user'  import { articles } from './module/articles'    const rootReducer = (history) => combineReducers({   // home,    user,    articles,    router: connectRouter(history)  })

2.1.3 圖片處理

  • 把一些靜態文件再用 photoshop 換一種格式或者壓縮了一下, 比如 logo 圖片,原本 111k,壓縮後是 23K。

  • 首頁的文章列表圖片,修改為懶載入的方式載入。

之前因為不想為了個懶載入功能而引用一個插件,所以想自己實現,看了網上關於圖片懶載入的一些程式碼,再結合本項目,實現了一個圖片懶載入功能,加入了 事件的節流(throttle)與防抖(debounce)

程式碼如下:

// fn 是事件回調, delay 是時間間隔的閾值  function throttle(fn, delay) {    // last 為上一次觸發回調的時間, timer 是定時器    let last = 0,      timer = null;    // 將throttle處理結果當作函數返回    return function() {      // 保留調用時的 this 上下文      let context = this;      // 保留調用時傳入的參數      let args = arguments;      // 記錄本次觸發回調的時間      let now = +new Date();        // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值      if (now - last < delay) {        // 如果時間間隔小於我們設定的時間間隔閾值,則為本次觸發操作設立一個新的定時器        clearTimeout(timer);        timer = setTimeout(function() {          last = now;          fn.apply(context, args);        }, delay);      } else {        // 如果時間間隔超出了我們設定的時間間隔閾值,那就不等了,無論如何要回饋給用戶一次響應        last = now;        fn.apply(context, args);      }    };  }    // 獲取可視區域的高度  const viewHeight = window.innerHeight || document.documentElement.clientHeight;  // 用新的 throttle 包裝 scroll 的回調  const lazyload = throttle(() => {    // 獲取所有的圖片標籤    const imgs = document.querySelectorAll('#list .wrap-img img');    // num 用於統計當前顯示到了哪一張圖片,避免每次都從第一張圖片開始檢查是否露出    let num = 0;    for (let i = num; i < imgs.length; i++) {      // 用可視區域高度減去元素頂部距離可視區域頂部的高度      let distance = viewHeight - imgs[i].getBoundingClientRect().top;      // 如果可視區域高度大於等於元素頂部距離可視區域頂部的高度,說明元素露出      if (distance >= 100) {        // 給元素寫入真實的 src,展示圖片        let hasLaySrc = imgs[i].getAttribute('data-has-lazy-src');        if (hasLaySrc === 'false') {          imgs[i].src = imgs[i].getAttribute('data-src');          imgs[i].setAttribute('data-has-lazy-src', true); //        }        // 前 i 張圖片已經載入完畢,下次從第 i+1 張開始檢查是否露出        num = i + 1;      }    }  }, 1000);

注意:給元素寫入真實的 src 了之後,把 data-has-lazy-src 設置為 true ,是為了避免回滾的時候再設置真實的 src 時,瀏覽器會再請求這個圖片一次,白白浪費伺服器頻寬。

具體細節請看文件 文章列表

2.2 後端優化

後端用到的技術是 node、express 和 mongodb。

後端主要問題是介面速度很慢,特別是文章列表的介面,已經是分頁請求數據了,為什麼還那麼慢呢 ?

所以查看了介面返回內容之後,發現返回了很多列表不展示的欄位內容,特別是文章內容都返回了,而文章內容是很大的,佔用了很多資源與頻寬,從而使介面消耗的時間加長

列表

從上圖可以看出文章列表介面只要返迴文章的 標題、描述、封面、查看數,評論數、點贊數和時間即可。

所以把不需要給前端展示的欄位注釋掉或者刪除。

// 待返回的欄位        let fields = {          title: 1,          // author: 1,          // keyword: 1,          // content: 1,          desc: 1,          img_url: 1,          tags: 1,          category: 1,          // state: 1,          // type: 1,          // origin: 1,          // comments: 1,          // like_User_id: 1,          meta: 1,          create_time: 1,          // update_time: 1,        };

同樣對其他的介面都做了這個處理。

後端做了處理之後,所有的介面速度都加快了,特別是文章列表介面,只用了 0.04 – 0.05 秒左右,相比之前的 4.3 秒,速度提高了 100 倍,簡直不要太爽, 效果如下:

image.png

此刻心情如下:

2.3 伺服器優化

你以為前後端都優化一下,本文就完了 ?小兄弟,你太天真了,重頭戲在後頭 !

筆者伺服器用了 nginx 代理。

做的優化如下:

  • 隱藏 nginx 版本號

一般來說,軟體的漏洞都和版本相關,所以我們要隱藏或消除 web 服務對訪問用戶顯示的各種敏感資訊。

如何查看 nginx 版本號? 直接看 network 的介面或者靜態文件請求的 Response Headers 即可。

沒有設置之前,可以看到版本號,比如我網站的版本號如下:

Server: nginx/1.6.2

設置之後,直接顯示 nginx 了,沒有了版本號,如下:

Server: nginx
  • 開啟 gzip 壓縮

nginx 對於處理靜態文件的效率要遠高於 Web 框架,因為可以使用 gzip 壓縮協議,減小靜態文件的體積加快靜態文件的載入速度、開啟快取和超時時間減少請求靜態文件次數。

筆者開啟 gzip 壓縮之後,請求的靜態文件大小大約減少了 2 / 3 呢。

gzip on;  #該指令用於開啟或關閉gzip模組(on/off)    gzip_buffers 16 8k;  #設置系統獲取幾個單位的快取用於存儲gzip的壓縮結果數據流。16 8k代表以8k為單位,安裝原始數據大小以8k為單位的16倍申請記憶體    gzip_comp_level 6;  #gzip壓縮比,數值範圍是1-9,1壓縮比最小但處理速度最快,9壓縮比最大但處理速度最慢    gzip_http_version 1.1;  #識別http的協議版本    gzip_min_length 256;  #設置允許壓縮的頁面最小位元組數,頁面位元組數從header頭得content-length中進行獲取。默認值是0,不管頁面多大都壓縮。這裡我設置了為256    gzip_proxied any;  #這裡設置無論header頭是怎麼樣,都是無條件啟用壓縮    gzip_vary on;  #在http header中添加Vary: Accept-Encoding ,給代理伺服器用的    gzip_types      text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml      text/javascript application/javascript application/x-javascript      text/x-json application/json application/x-web-app-manifest+json      text/css text/plain text/x-component      font/opentype font/ttf application/x-font-ttf application/vnd.ms-fontobject      image/x-icon;  #進行壓縮的文件類型,這裡特別添加了對字體的文件類型    gzip_disable "MSIE [1-6].(?!.*SV1)";  #禁用IE 6 gzip

把上面的內容加在 nginx 的配置文件 ngixn.conf 裡面的 http 模組裡面即可。

是否設置成功,看文件請求的 Content-Encoding 是不是 gzip 即可。

  • 設置 expires,設置快取
 server {          listen       80;          server_name  localhost;          location  / {              root   /home/blog/blog-react/build/;              index  index.html;              try_files $uri $uri/ @router;              autoindex on;              expires 7d; # 快取 7 天          }      }

我重新刷新請求的時候是 2019 年 3 月 16 號,是否設置成功看如下幾個欄位就知道了:

  1. Staus Code 裡面的 form memory cache 看出,文件是直接從本地瀏覽器本地請求到的,沒有請求伺服器。
  2. Cache-Control 的 max-age= 604800 看出,過期時間為 7 天。
  3. Express 是 2019 年 3 月 23 號過期,也是 7 天過期。

注意:上面最上面的用紅色圈中的 Disable cache 是否是打上了勾,打了勾表示:瀏覽器每次的請求都是請求伺服器,無論本地的文件是否過期。所以要把這個勾去掉才能看到快取的效果。

終極大招:服務端渲染 SSR,也是筆者接下來的方向。

3.1 測試場景

一切優化測試的結果脫離了實際的場景都是在耍流氓,而且不同時間的網速對測試結果的影響也是很大的。

所以筆者的測試場景如下:

  • a. 筆者的伺服器是阿里的,配置是入門級的學生套餐配置,如下:

伺服器配置

  • b. 測試網路為 10 M 光纖寬頻。

3.2 優化結果

優化之後的首屏速度是 2.07 秒。

最後加了快取的結果為 0.388 秒

image.png

再來看下 Lighthouse 的測試結果:

image.png

比起優化之前,各項指標都提升了很大的空間。

4. 最後

優化之路漫漫,永無止境,天下武功,唯快不破。

本次優化的前端與後端項目,都已經開源在 github 上了,歡迎圍觀。

前端:https://github.com/biaochenxuying/blog-react

後端:https://github.com/biaochenxuying/blog-node

github 部落格地址:https://github.com/biaochenxuying/blog

如果您覺得這篇文章不錯或者對你有所幫助,請給個贊或者星唄,你的點贊就是我繼續創作的最大動力。

全棧修鍊