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


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

1、 dll動態鏈接庫, 使用DllPlugin DllReferencePlugin,將第三方庫提取出來另外打包出來,然后動態引入html。可以提高打包速度和緩存第三方庫
這種方式打包可以見京東團隊的gaea方案
https://www.npmjs.com/package/gaea-cli

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,直接使用上面提供的變量

上面沒有import vue進來,項目中照常使用Vue這個全局變量。
既然沒有import vue 自然不會打包vue,然后你會發現你的vendor.js會從700kb+ 減少到 30-40kb,非常棒的優化。
關於是否注釋掉,這里有兩張驗證圖,在webpack配置了externals的情況下


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

  

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

固定module id,為了緩存

chunk: 是指代碼中引用的文件(如:js、css、圖片等)會根據配置合並為一個或多個包,我們稱一個包為 chunk。
module: 是指將代碼按照功能拆分,分解成離散功能塊。拆分后的代碼塊就叫做 module。可以簡單的理解為一個 export/import 就是一個 module。
解決方案:
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出了問題,直接映射問題源

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

第二種:
原理:根據每個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-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按需加載。關於開啟與不開啟useBuiltIn構建包的大小詳細見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壓縮要使用的圖片。 詳細腳本見 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


開啟后會大大加快首頁加載時長,效果非常不錯。

圖片懶加載

放個鏈接吧
https://juejin.im/post/5bbc60e8f265da0af609cd04

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM