vue客戶端渲染首屏優化之道

陳磊同學發表於2019-04-17

提取第三方庫,快取,減少打包體積

1、 dll動態連結庫, 使用DllPlugin DllReferencePlugin,將第三方庫提取出來另外打包出來,然後動態引入html。可以提高打包速度和快取第三方庫 這種方式打包可以見京東團隊的gaea方案 www.npmjs.com/package/gae…

2、webpack4的splitChunks或者 webpack3 CommonsChunkPlugin 配合 externals (資源外接) 主要是分離 第三方庫,自定義模組(引入超過3次的自定義模組被分離),webpack執行程式碼(runtime,minifest)。 配合externals,意思將第三方庫外接,用cdn的形式引入,可以減少打包體積。 詳細程式碼 在webpack.config.js(peoduction環境下)

externals: {
    'vue': 'Vue', //vue 是包名 Vue是引入的全域性變數
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'axios': 'axios',
    'iview': 'iview' //iview
},
複製程式碼

然後再main.js或者任何地方不再引入 比如vue,直接使用上面提供的變數

vue客戶端渲染首屏優化之道
上面沒有import vue進來,專案中照常使用Vue這個全域性變數。 既然沒有import vue 自然不會打包vue,然後你會發現你的vendor.js會從700kb+ 減少到 30-40kb,非常棒的優化。

webpack4 splitChunk的配置

//提取node_modules裡面的三方模組
module.exports = {
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    chunks: "initial",
                    test: path.resolve(__dirname, "node_modules") // 路徑在 node_modules 目錄下的都作為公共部分
          name: "vendor", // 使用 vendor 入口作為公共部分
                    enforce: true,
                },
            },
        },
    },
}
//提取 manifest (webpack執行程式碼)
{
    runtimeChunk: true;
}
複製程式碼

webpack3 CommonsChunkPlugin 的配置,寫在plugins中

 // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),
複製程式碼

下面兩個問題詳細見 www.jianshu.com/p/23dcabf35…

固定module id,為了快取

chunk: 是指程式碼中引用的檔案(如:js、css、圖片等)會根據配置合併為一個或多個包,我們稱一個包為 chunk。 module: 是指將程式碼按照功能拆分,分解成離散功能塊。拆分後的程式碼塊就叫做 module。可以簡單的理解為一個 export/import 就是一個 module。 解決方案: webpack4 HashedModuleIdsPlugin 或者 webpack4 的 optimization.moduleIds='hash'

固定chunk id

我們在固定了 module id 之後同理也需要固定一下 chunk id,不然我們增加 chunk 或者減少 chunk 的時候會和 module id 一樣,都可能會導致 chunk 的順序發生錯亂,從而讓 chunk 的快取都失效。 提供了一個叫NamedChunkPlugin的外掛,但在使用路由懶載入的情況下,你會發現NamedChunkPlugin並沒什麼用。 原因: 使用自增 id 的情況下是不能保證你新新增或刪除 chunk 的位置的,一旦它改變了,這個順序就錯亂了,就需要重排,就會導致它之後的所有 id 都發生改變了。 下面兩種解決方案 第一種: 在 webpack2.4.0 版本之後可以自定義非同步 chunk 的名字了,例如:

import(/* webpackChunkName: "my-chunk-name" */ "module");
複製程式碼

我們在結合 vue 的懶載入可以這樣寫。

{
    path: '/test',
    component: () => import(/* webpackChunkName: "test" */ '@/views/test')
  },
複製程式碼

還要記得配置chunkFilename

output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: config.publicPath + '/',//靜態檔案的處理,生產環境有效.開發環境其實是從記憶體中拿檔案的
            filename: 'js/[name].[chunkhash].js',
            chunkFilename: 'js/[name].[chunkhash].js' //寫成[name].xxxx,便於查詢chunk源  詳細見 NamedChunkPlugin 
        },
複製程式碼

打包之後就生成了名為 test的 chunk 檔案, chunk 有了 name 之後就可以解決NamedChunksPlugin沒有 name 的情況下的 bug 了。檢視打包後的程式碼我們發現 chunkId 就不再是一個簡單的自增 id 了。 推薦第一種,既可以固定chunk id(用的chunkname代替),又可以瞭解專案打包詳情比如遇到大檔案,到底是哪個chunk出了問題,直接對映問題源

vue客戶端渲染首屏優化之道
我們可以直接看到786kb的大檔案是來自於 test1.vue和test2.vue的vendor包(第三方庫),然後進入test1.vue,echarts就是問題源,關於解決就是把echarts等第三方庫外接。詳細見上面資源外接。
vue客戶端渲染首屏優化之道

第二種: 原理:根據每個chunk裡面的module id 去唯一化這個chunk的name,只要裡面的module沒有增多或減小,那麼它的名字是不會變的

new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  return Array.from(chunk.modulesIterable, m => m.id).join("_");
});

複製程式碼

當然這個方案還是有一些弊端的因為 id 會可能很長,如果一個 chunk 依賴了很多個 module 的話,id 可能有幾十位,所以我們還需要縮短一下它的長度。我們首先將拼接起來的 id hash 以下,而且要保證 hash 的結果位數也能太長,浪費位元組,但太短又容易發生碰撞,所以最後我們我們選擇 4 位長度,並且手動用 Set 做一下碰撞校驗,發生碰撞的情況下位數加 1,直到碰撞為止。詳細程式碼如下:

const seen = new Set();
const nameLength = 4;

new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  const modules = Array.from(chunk.modulesIterable);
  if (modules.length > 1) {
    const hash = require("hash-sum");
    const joinedHash = hash(modules.map(m => m.id).join("_"));
    let len = nameLength;
    while (seen.has(joinedHash.substr(0, len))) len++;
    seen.add(joinedHash.substr(0, len));
    return `chunk-${joinedHash.substr(0, len)}`;
  } else {
    return modules[0].id;
  }
});

複製程式碼

提取css為單獨檔案並壓縮

webpack4 的 mini-css-extract-plugin
webpack3的ExtractTextPlugin

壓縮js檔案

webpack3 UglifyJsPlugin webpack4 自帶了UglifyJsPlugin功能,無需配置,需要開啟mode production

tree shaking和sideEffects

去除沒有被引用的程式碼, webpack4預設支援,。 因為Tree Shaking這個功能是基於ES6 modules 的靜態特性檢測,來找出未使用的程式碼,所以如果你使用了 babel 外掛的時候,如:babel-preset-env,它預設會將模組打包成commonjs,這樣就會讓Tree Shaking失效了。

sideEffects是webpack4才有的功能,目的是對第三方沒有任何副作用的庫進行按需載入。 webpack 的 sideEffects 可以幫助解決這個問題。現在 lodash 的 ES 版本 的 package.json 檔案中已經有 sideEffects: false 這個宣告瞭,當某個模組的 package.json 檔案中有了這個宣告之後,webpack 會認為這個模組沒有任何副作用,只是單純用來對外暴露模組使用,那麼在打包的時候就會做一些額外的處理。 例如你這麼使用 lodash:


import { forEach, includes } from 'lodash-es'

forEach([1, 2], (item) => {
    console.log(item)
})

console.log(includes([1, 2, 3], 1))
複製程式碼

由於 lodash-es 這個模組的 package.json 檔案有 sideEffects: false 的宣告,所以 webpack 會將上述的程式碼轉換為以下的程式碼去處理:

import { default as forEach } from 'lodash-es/forEach'
import { default as includes } from 'lodash-es/includes'
// ... 其他程式碼

複製程式碼

最終 webpack 不會把 lodash-es 所有的程式碼內容打包進來,只是打包了你用到的那兩個方法,這便是 sideEffects 的作用。

懶載入 import()

babel需要配置@babel/plugin-syntax-dynamic-import
按需載入 import(/webpackChunkName: "Index"/"xxx.vue")
命名設定規則在chunkFilename (如果沒有設定,則按照預設的1.xxxx.js這樣命名,其實也會分開打包,便於除錯,打包時看到某個chunk比較大,可以檢視該chunk對應的vue檔案) chunkFilename: utils.assetsPath('js/[name].[chunkhash].js') 既然按需載入,就不會打包到 app.js(主entry chunk)中,肯定會分開打包,然後按需載入

babel 按需引入pollyfill

Babel 預設只轉換 JavaScript 語法,而不轉換新的 API,比如 Promise、Generator、Set、Maps、Symbol 等全域性物件,一些定義在全域性物件上的方法(比如 Object.assign)也不會被轉碼。如果想讓未轉碼的 API 可在低版本環境正常執行,這就需要使用 polyfill。

babel6當前最普遍的解決方案

使用transform-runtime或者babel-polyfill

比較transform-runtime與babel-polyfill引入墊片的差異: 使用transform - runtime是按需引入,需要用到哪些polyfill,runtime就自動幫你引入哪些,不需要再手動一個個的去配置plugins,只是引入的polyfill不是全域性性的,有些侷限性。而且runtime引入的polyfill不會改寫一些例項方法,比如Object和Array原型鏈上的方法,像前面提到的Array.protype.includes。

注意使用transform - runtime需要安裝babel - runtime,babel - runtime 是一個庫,用於引入的 放在--save 而 babel - plugin - transform - runtime是幫助引入babel - runtime這個庫的(自動的) babel - runtime和 babel - plugin - transform - runtime的區別是,相當一前者是手動擋而後者是自動擋,每當要轉譯一個api時都要手動加上require('babel-runtime') , 而babel - plugin - transform - runtime會由工具自動新增,主要的功能是為api提供沙箱的墊片方案,不會汙染全域性的api,因此適合用在第三方的開發產品中。 而重複引入會被webpack設定的commonChunkPlugin 給去重 babel - polyfill就能解決runtime的那些問題,它的墊片是全域性的,而且全能,基本上ES6中要用到的polyfill在babel - polyfill中都有,它提供了一個完整的ES6 + 的環境。babel官方建議只要不在意babel - polyfill的體積,最好進行全域性引入,因為這是最穩妥的方式。 一般的建議是開發一些框架或者庫的時候使用不會汙染全域性作用域的babel - runtime,而開發web應用的時候可以全域性引入babel - polyfill避免一些不必要的錯誤,而且大型web應用中全域性引入babel - polyfill可能還會減少你打包後的檔案體積(相比起各個模組引入重複的polyfill來說)。

以下為三種babel6解決ES6 API pollyfill的引入方式 ①全域性使用babel - polyfill(不設定babel - preset - env options項的useBuiltIns) 具體使用方法如下: a.直接在index.html檔案head中直接引入polyfill js或者CDN地址; b.在package.json中新增babel - polyfill依賴, 在webpack配置檔案增加入口: 如entry: ["babel-polyfill", './src/app.js'], polyfill將會被打包進這個入口檔案中, 必須放在檔案最開始的地方; c.在入口檔案頂部直接import ''babel - polyfill'; 此方案的優點是簡單、一次性可以解決瀏覽器的所有polyfill相容性問題,缺點就是一次性引入了ES6 + 的所有polyfill, 打包後的js檔案體積會偏大, 在現代瀏覽器上不需要全部的polyfill, 其次汙染了全域性物件,不太適合框架類的開發,框架類的開發建議下面的②方案。 注: polyfill.io庫會根據你的使用的瀏覽器做相應的polyfill, 可以極大的解決引入過大的問題。

② 全域性使用babel - polyfill(設定babel - preset - env options項的useBuiltIns) 具體使用方法如下:

  1. 引入babel - preset - env包;
  2. 在.babelrc檔案預設presets中使用設定babel - preset - env options項 useBuiltins: usage | entry (usage: 僅僅載入程式碼中用到的 polyfill.entry: 根據瀏覽器版本的支援,將 polyfill 需求拆分引入,僅引入有瀏覽器不支援的polyfill) targets.browsers: 瀏覽器相容列表 modules: false
  3. 在入口檔案頂部直接import ''babel - polyfill';

此方案適合應用級的開發,babel會根據指定的瀏覽器相容列表自動引入所有所需的polyfill。

③ 使用外掛 babel - runtime 或 babel - plugin - tranform - runtime babel - runtime會出現重複引用的問題,而babel - plugin - tranform - runtime抽離了公共模組, 避免了重複引入,下面的配置主要以babel - plugin - tranform - runtime來說。

  1. 引入babel - plugin - tranform - runtime包;
  2. 在.babelrc檔案plugins中新增babel - plugin - tranform - runtime: "plugins": ["transform-runtime"];
  3. 配合上面方法②中的第2步中的預設presets的設定;

此方案無全域性汙染,依賴統一按需引入(polyfill是各個模組共享的), 無重複引入, 無多餘引入,適合用來開發庫。 安裝包 "babel-core": "^6.22.1", "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-loader": "^7.1.1", "babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-transform-runtime": "^6.22.0", "babel-plugin-transform-vue-jsx": "^3.5.0", "babel-preset-env": "^1.3.2", "babel-preset-stage-2": "^6.22.0",

vue-cli的babel-cli的.babelrc


{
    "presets": [
        ["env", {
            "modules": false,
            "targets": {
                "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
            }
        }],
        "stage-2"
    ],
        "plugins": ["transform-vue-jsx", "transform-runtime"]
}
複製程式碼

babel7解決方案(注意core-js的版本)

pollyfill 按需載入 @babel/polyfill 模組包括 core-js 和一個自定義的 regenerator runtime 模組用於模擬完整的 ES2015+ 環境。 不再需要手動引入import ''babel - polyfill'; 只需要簡單的配置就能自動智慧化引入@babel/polyfill,設定useBuiltIns按需載入 .babelrc

{
    "presets": [
        ["@babel/preset-env",
            {
                "modules": false,
                "targets": {
                    "browsers": ["> 1%", "last 2 versions", "not ie <= 8", "Android >= 4", "iOS >= 8"]
                },
                "useBuiltIns": "usage"

            }]
    ],
        "plugins": [
            "@babel/plugin-syntax-dynamic-import"

        ]
}

複製程式碼

升級到7需要安裝關於@babel的包 "@babel/core": "^7.1.2", "@babel/plugin-syntax-dynamic-import": "7.0.0", //用於import() "@babel/polyfill": "7.0.0", "@babel/preset-env": "7.1.0", "babel-loader": "8.0.4", babel使用總結,建議使用babel7,構建速度更快,建議使用@babel/preset-env",建議開啟useBuiltIns屬性,讓babel-polyfill按需載入。關於開啟與不開啟的構建包的大小詳細見https://github.com/ab164287643/studyBabel/tree/master/7-babel-env

webpack3和webpack4的差異比較

1、增加了mode配置,只有兩種值development | production,對不同的環境他會啟用不同的配置。

2、預設生產環境開起了很多程式碼優化(minify, splite)

3、 開發時開啟注視和驗證,並加上了evel devtool

4、 生產環境不支援watching,開發環境優化了打包的速度

5、 生產環境開啟模組串聯(原ModulecondatenationPlugin)

6、自動設定process.env.NODE_EVN到不同環境,也就是不使用DefinePlugin了

7 、如果mode設定none,所有預設設定都去掉了。

8、在webpack4之前,我們處理公共模組的方式都是使用CommonsChunkPlugin,然後該外掛的讓開發這配置繁瑣,並且公共程式碼的抽離,不夠徹底和細緻,因此新的splitChunks改進了這些能力。

9、預設開啟 uglifyjs - webpack - plugin 的 cache 和 parallel,即快取和並行處理,這樣能大大提高 production mode 下壓縮程式碼的速度。

生產環境和開發環境各自增加很多預設配置(比如UglifyJsPlugin預設用於生產環境),打包速度更快

圖片壓縮

使用tinify壓縮要使用的圖片。 詳細指令碼見 gitee.com/cchennlleii…

關於圖片格式優化

jpeg 有失真壓縮,體積小,不支援透明。 png 無失真壓縮,高保真,支援透明。 png - 8 2 ^ 8種色彩 256種 png - 24 2 ^ 24種色彩 1600w種 png - 32 2 ^ 24 ^ 8種 (還有8種透明度色彩通道) 更多 顏色支援越多,體積越大 svg 向量圖 體積小 不失真,適用於小圖示 base64 減小http請求,但不宜處理大圖片,因為大圖片增加頁面大小,webpack的url - loader已經支援 webP 新興格式,支援有損和無失真壓縮,支援透明,體積還特別小,與 PNG 相比,通常提供 3 倍的檔案大小,瀏覽器相容性低,侷限性較大。 專案中的支援webp(參照自京東gaea): 在index.html中判斷是否支援webP

window.supportWebp = false;
if (document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0) {
    document.body.classList.add('webp');
    window.supportWebp = true;
}
複製程式碼

然後用上面的圖片壓縮指令碼壓縮圖片,會在img下面生成一個webp檔案,裡面就是轉換後的webp格式的圖片。 css中寫兩套樣式,比如 .banner{ background - image: url("xxxx.png") } .webp.banner{ background - image: url("xxxx.webp") } 在js中根據window.supportWebp去判斷用那種圖片。 (都寫到這裡了不用再多少了吧)

開啟gziped

使用compression - webpack - plugin 在開發環境下開啟

if (config.productionGzip) {
    const CompressionWebpackPlugin = require('compression-webpack-plugin');
    //增加瀏覽器CPU(需要解壓縮), 減少網路傳輸量和頻寬消耗 (需要衡量,一般小檔案不需要壓縮的)
    //圖片和PDF檔案不應該被壓縮,因為他們已經是壓縮的了,試著壓縮他們會浪費CPU資源而且可能潛在增加檔案大小。
    webpackConfig.plugins.push(
        new CompressionWebpackPlugin({
            asset: '[path].gz[query]',
            algorithm: 'gzip',
            test: /\.(js|css)$/,
            threshold: 10240,//達到10kb的靜態檔案進行壓縮 按位元組計算
            minRatio: 0.8,//只有壓縮率比這個值小的資源才會被處理
            deleteOriginalAssets: false//使用刪除壓縮的原始檔
        })
    )
}
複製程式碼

當開啟gziped壓縮後,伺服器需要做相應的配置,讓伺服器端可以傳輸壓縮後的檔案。 開啟 nginx 服務端 gzip效能優化。找到nginx配置檔案在 http 配置裡面新增如下程式碼,然後重啟nginx服務即可。

http: {
    gzip on;
    gzip_static on;
    gzip_buffers 4 16k;
    gzip_comp_level 5;
    gzip_types text / plain application / javascript text / css application / xml text / javascript application / x - httpd - php image / jpeg
    image / gif image / png;
}
複製程式碼

開啟apache gziped壓縮 在 http.conf裡面配置 找到下面這句去掉#

LoadModule deflate_module modules / mod_deflate.so
複製程式碼

然後在最後面加上,記住不壓縮圖片

    < IfModule mod_deflate.c >
# 告訴 apache 對傳輸到瀏覽器的內容進行壓縮
SetOutputFilter DEFLATE
# 壓縮等級 9
DeflateCompressionLevel 9
#設定不對字尾gif,jpg,jpeg,png的圖片檔案進行壓縮
SetEnvIfNoCase Request_URI.(?: gif | jpe ? g | png)$ no - gzip dont - vary
</IfModule >
複製程式碼
可以看到如下效果,http傳輸大小為173kb,而解壓縮後大小為619kb
複製程式碼

vue客戶端渲染首屏優化之道
開啟後會大大加快首頁載入時長,效果非常不錯。

圖片懶載入

放個連結吧 juejin.im/post/5bbc60…

本文所有配置程式碼
gitee.com/cchennlleii…

相關文章