前端性能優化:webpack分離 + LocalStorage緩存

一,優化背景

上一篇關於webpack優化的文章webpack + react 優化:縮小js包體積談到如何縮小webpack打包後的js代碼體積,來減少網絡請求數據量,這次嘗試將第三方庫(React,ajax等)從業務代碼中分離出來,並且將分離出來的第三方庫緩存在LocalStorage中。

該次優化的出發點有下面兩點:

①每次更改業務代碼都會打包成新的bundle.js,前端需要拋棄以前的HTTP緩存重新下載249K左右的bundle.js,即使改變的業務代碼很少。
②嘗試使用LocalStorage(移動端上兼容很好)

第一點是痛點,第二點是作爲前端工程師學習研究用的,希望足夠嚴謹不會給用戶帶來副作用。

優化結果

①webpack分離出業務代碼和第三方庫

分離前:
webpack代碼切分

分離後:bundle.js 爲業務代碼,vendor.js爲第三方庫代碼(幾乎不更新)
webpack代碼切分

②使用LocalStorage緩存第三方庫到本地

緩存前:
localstorage緩存前

緩存後:(少下了198KB的libs.js,注:這裏的app對應上面的bundle.js,libs對應vendor.js)
localstorage緩存前

下面上相關代碼。

二,優化思路

①將公用庫分出來

這一部給出webpack配置文件即可,注意看註釋部分:

var webpack = require('webpack');
var path = require('path');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  entry: {
    //業務代碼
    bundle: path.resolve(__dirname, 'src/main.jsx'), 
    //第三方庫
    vendor: ["react","react-dom","react-router","@fdaciuk/ajax"]
  },
  //輸出路徑
  output: {
    path: __dirname + '/server/public',
    publicPath: '/',
    filename: './[name].js'
  },
  module: {
    loaders:[
      { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") },
      { test: /\.scss$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader!sass-loader") },
      { test: /\.(jpg|png)$/, loader: "url" },
      { test: /\.js[x]?$/, include: path.resolve(__dirname, 'src'), exclude:/node_modules/, loader: 'babel-loader' },
      { test: /\.woff$/, loader: "url-loader?prefix=font/&limit=5000&mimetype=application/font-woff" },
      { test: /\.ttf$/, loader: "file-loader?prefix=font/" },
      { test: /\.svg$/, loader: "file-loader?prefix=font/" }
    ]
  },
  resolve: {
    extensions: ['', '.web.js', '.js', '.json', '.jsx'],
    modulesDirectories: ['node_modules', path.join(__dirname, '../node_modules')],
  },
  plugins: [
    new webpack.optimize.DedupePlugin(),
    new ExtractTextPlugin("bundle.css"),
    //將第三方庫打包到vendor.js
    new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }),
    new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV), }, }),
    new webpack.optimize.UglifyJsPlugin({ output: { comments: false, }, compress: { warnings: false } }),
  ]
};

關於這部分深入的理解和學習可以看webpack的示例.

②將公用庫緩存在LoaclStorage中

這一步比較好玩,涉及到的東西也比較多,先上學習參考資料:

  1. MDN:LocalStorage
  2. 知乎:靜態資源(JS/CSS)存儲在localStorage有什麼缺點?爲什麼沒有被廣泛應用?
  3. Dynamic script addition should be ordered?(動態添加的script標籤會按順序執行嗎?)

關於這一部分,有兩個要點:

  • LocalStorage緩存
  • LocalStorage緩存版本控制

按照慣例:上源碼

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<title>Examples</title>
<meta name="description" content="">
<meta name="keywords" content="">
<!-- 利用noscript, 使script標籤不加載,不執行 -->
<noscript>
  <script src="vendor.js"></script>
  <script src="bundle.js"></script>
</noscript>
<script>
  //這則匹配獲取兩個script的src
  var script_info = document.head.querySelector('noscript').innerText.split(/\s/).join('').match(/src="(.+)".*src="(.+)"/);
  var vendor_src = script_info[1], bundle_src = script_info[2];
  var vendor_script = document.createElement('script'), 
      bundle_script = document.createElement('script'); vendor_script.defer = true;
  bundle_script.defer = true, bundle_script.src = bundle_src;
  if (window.localStorage) {
    var cur_version = vendor_src, vendor = null;
    if (window.localStorage.getItem("vendor_version") == cur_version && window.localStorage.getItem("vendor")) {
      //命中緩存
      vendor = window.localStorage.getItem("vendor");
      vendor_script.innerText = vendor;
      document.head.appendChild(vendor_script);
      document.head.appendChild(bundle_script);
    } else {
      //獲取js代碼
      var httpRequest = new XMLHttpRequest();
      httpRequest.onreadystatechange = function(){
        if (httpRequest.readyState === XMLHttpRequest.DONE && httpRequest.status === 200 || httpRequest.status === 304) {
          vendor = httpRequest.responseText;
          vendor_script.innerText = vendor;
          document.head.appendChild(vendor_script);
          document.head.appendChild(bundle_script);
          //以當前vendor的src鏈接作爲key用於版本控制
          window.localStorage.setItem("vendor_version", cur_version);
          //存儲實際代碼
          window.localStorage.setItem("vendor", vendor);
        }
        //should handle 500
      }
      httpRequest.open('GET', vendor_src, true);
      httpRequest.send(null);
    }
  } else {
    //不支持localstorage時的回退方案,直接將script標籤添加到head
    vendor_script.src = vendor_script;
    document.head.appendChild(vendor_script);
    document.head.appendChild(bundle_script);
  }
</script>
<link href="bundle.css" rel="stylesheet">
</head>
<body>
  DEMO
</body>
</html>

代碼沒有什麼高深的地方(畢竟水平有限,但是簡單易懂有用的代碼也挺好的),應該看完就知道大概了,可能你會疑惑:

爲什麼用了<noscript>標籤?

這裏回答一下,也是今早折騰了一小下的小亮點。

我的web後臺使用了rails,我在後臺給靜態資源文件名加上了指紋作爲緩存的依據,實際如下:

rails 後臺模板:會自動找到加上自問的靜態資源。
rails 後臺模板

生成的HTML:
noscript

可見一旦文件內容發生變化,對應的文件名也發生了改變(HTTP緩存失效),因此可以用文件名來做LocalStorage緩存的版本。也正是因爲文件名不確定,因此只能通過去找到相應的script標籤的src才能獲取資源。

那麼問題就來,一開始我把vendor和bundle的script標籤放在控制緩存的腳本後面,但是執行到緩存腳本時,頁面還沒解析到後面兩個script標籤,因此無法獲取script的src。而把vendor和bundle的script標籤放在控制緩存的腳本前面時,在執行緩存腳本前vendor和bundle腳本就已經開始下載了(通過設置type=”text”或其他可以使script不執行),不夠優雅。

解決思路如下:

  1. 頁面還沒解析到後面兩個script標籤時能獲取後面的信息嗎?(個人覺得不合常理,如果有方法,請分享,萬分感謝)
  2. 有什麼辦法能讓前面的script不下載? => <noscript>包裹即可,well done.

三,優化結果 && more

優化結果其實前面已經提及了,無非是首次加載web應用後就不需要再加載不常改變的libs.js(200k左右),值得注意的是,前面緩存前後的比較其實不嚴謹,因爲都把HTTP cache關了,實際場景中設置了HTTP cache,所以其實LocalStorage優化不大,但是節省了用戶流量是實實在在的。

優化完之後還有些疑惑:

  1. 爲什麼vendor.js體積比所有庫(["react","react-dom","react-router","@fdaciuk/ajax"])加起來大?vendor.js裏面肯定包含了webpack實現模塊化的一些代碼,但佔這麼大應該不科學,抽空應該好好看看webpack的實現原理。
  2. LocalStorage是否比HTTP cache更加可靠?

四,一些想法

除了將第三方庫緩存下來之外,是不是也可以將一些萬年不變的圖片緩存下來?例如各個分類都有個大圖,很耗流量,現在的做法是減少尺寸,在清晰度上犧牲了一些。

最近也在看事件循環,zepto.js的event.js和touch.js,好多可以學習深入的地方,興奮之餘也在提醒自己,要踏踏實實攻城拔寨。

人生是一場馬拉松,今日的領先和落後都不值一提,唯一值得關注的是你自身的不斷成長。

自勉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章