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

提取第三方庫,緩存,減小打包體積

一、 dll動態連接庫, 使用DllPlugin DllReferencePlugin,將第三方庫提取出來另外打包出來,而後動態引入html。能夠提升打包速度和緩存第三方庫
這種方式打包能夠見京東團隊的gaea方案
https://www.npmjs.com/package/gaea-clijavascript

二、webpack4的splitChunks或者 webpack3 CommonsChunkPlugin 配合 externals (資源外置)
主要是分離 第三方庫,自定義模塊(引入超過3次的自定義模塊被分離),webpack運行代碼(runtime,minifest)。
配合externals,意思將第三方庫外置,用cdn的形式引入,能夠減小打包體積。
詳細代碼
在webpack.config.js(peoduction環境下)php

externals: {
    'vue': 'Vue', //vue 是包名 Vue是引入的全局變量
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'axios': 'axios',
    'iview': 'iview' //iview
},

而後再main.js或者任何地方再也不引入 好比vue,直接使用上面提供的變量

上面沒有import vue進來,項目中照常使用Vue這個全局變量。
既然沒有import vue 天然不會打包vue,而後你會發現你的vendor.js會從700kb+ 減小到 30-40kb,很是棒的優化。
關因而否註釋掉,這裏有兩張驗證圖,在webpack配置了externals的狀況下


webpack4 splitChunk的配置css

//提取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中html

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

下面兩個問題詳細見 https://www.jianshu.com/p/23dcabf35744vue

固定module id,爲了緩存

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

固定chunk id

咱們在固定了 module id 以後同理也須要固定一下 chunk id,否則咱們增長 chunk 或者減小 chunk 的時候會和 module id 同樣,均可能會致使 chunk 的順序發生錯亂,從而讓 chunk 的緩存都失效。
提供了一個叫NamedChunkPlugin的插件,但在使用路由懶加載的狀況下,你會發現NamedChunkPlugin並沒什麼用。
緣由:
使用自增 id 的狀況下是不能保證你新添加或刪除 chunk 的位置的,一旦它改變了,這個順序就錯亂了,就須要重排,就會致使它以後的全部 id 都發生改變了。
下面兩種解決方案
第一種:
在 webpack2.4.0 版本以後能夠自定義異步 chunk 的名字了,例如:node

import(/* webpackChunkName: "my-chunk-name" */ "module");

咱們在結合 vue 的懶加載能夠這樣寫。webpack

{
    path: '/test',
    component: () => import(/* webpackChunkName: "test" */ '@/views/test')
  },

還要記得配置chunkFilenameios

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出了問題,直接映射問題源

咱們能夠直接看到786kb的大文件是來自於 test1.vue和test2.vue的vendor包(第三方庫),而後進入test1.vue,echarts就是問題源,關於解決就是把echarts等第三方庫外置。詳細見上面資源外置。
nginx

第二種:
原理:根據每一個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-runtimebabel-polyfill引入墊片的差別:
使用transform - runtime是按需引入,須要用到哪些polyfill,runtime就自動幫你引入哪些,不須要再手動一個個的去配置plugins,只是引入的polyfill不是全局性的,有些侷限性。並且runtime引入的polyfill不會改寫一些實例方法,好比Object和Array原型鏈上的方法,像前面提到的Array.protype.includes

注意使用transform-runtime須要安裝babel-runtimebabel-runtime 是一個庫,用於引入的 ,放在--save 而 babel-plugin-transform-runtime是幫助引入babel-runtime這個庫的(自動的)
babel-runtimebabel-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-runtimebabel-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按需加載。關於開啓與不開啓useBuiltIn構建包的大小詳細見https://github.com/ab164287643/studyBabel/tree/master/7-babel-env

webpack3和webpack4的差別比較

一、增長了mode配置,只有兩種值development | production,對不一樣的環境他會啓用不一樣的配置。
二、默認生產環境開起了不少代碼優化(minify, splite)
三、 開發時開啓注視和驗證,並加上了evel devtool
四、 生產環境不支持watching,開發環境優化了打包的速度
五、 生產環境開啓模塊串聯(原ModulecondatenationPlugin)
六、自動設置process.env.NODE_EVN到不一樣環境,也就是不使用DefinePlugin了
7 、若是mode設置none,全部默認設置都去掉了。
八、在webpack4以前,咱們處理公共模塊的方式都是使用CommonsChunkPlugin,而後該插件的讓開發這配置繁瑣,而且公共代碼的抽離,不夠完全和細緻,所以新的splitChunks改進了這些能力。
九、默認開啓 uglifyjs - webpack - plugin 的 cache 和 parallel,即緩存和並行處理,這樣能大大提升 production mode 下壓縮代碼的速度。
生產環境和開發環境各自增長不少默認配置(好比UglifyJsPlugin默認用於生產環境),打包速度更快

圖片壓縮

使用tinify壓縮要使用的圖片。 詳細腳本見 https://gitee.com/cchennlleii/MyTest/blob/master/webpack/.tinypng.js(此腳原本自於京東gaea)

關於圖片格式優化

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


開啓後會大大加快首頁加載時長,效果很是不錯。

圖片懶加載

放個連接吧
http://www.javashuo.com/article/p-rtmnvocl-ct.html

本文全部配置代碼
https://gitee.com/cchennlleii/MyTest/tree/master/webpack

相關文章
相關標籤/搜索