webpack效能優化(下)

wdBly發表於2018-11-26

在webpack效能優化(上)中,我們從 程式碼分離,Loader, webpack解析(resolve), webpack 外部擴充套件(Externals) ,Dlls 優化構建速度,等方面分析了優化手段,這篇文章讓我們接著來擼。

圖片等靜態檔案 dev prod

通常來說,我們會通過使用file-loader,url-loader等loader來處理專案中的靜態檔案,如圖片字型等檔案

//這樣最終dist檔案中就會生成font資料夾存放字型檔案
{
    test: /\.(woff|svg|eot|ttf)\??.*$/,
    loader: "url-loader",
    options: {
        limit: 8192,
        name: "font/[name].[hash:6].[ext]"
    }
}

複製程式碼

limit屬性是在檔案大小超出limit的值才會單獨打包,否則使用base64 的方式引用通常適用於小圖片,這就是我們通常的檔案處理方式。

使用base64引入圖片的好處是減少http請求數,但相應的問題是base64佔用的空間比普通的圖片檔案大一點。

當然我們還有另外一種方案,具體做法是將專案的中靜態檔案統一存放在static資料夾下,最後使用 CopyWebpackPlugin將static資料夾拷貝到dist目錄下

new CopyWebpackPlugin([
    {
        from: path.resolve(SRC_PATH, 'img'),
        to: 'img'
    }
]),
複製程式碼

這樣做的好處是我們的靜態資源不經過webpack的處理,可以提升構建速度,但問題也是很明顯的,那就是維護的成本增大並可能出現一些意外的情況,比如:

這樣處理的問題是可能開發環境引用路徑和打包檔案訪問圖片路徑不一致問題,這裡可以通過output.publicPath屬性來配置解決

output: {
    //打包檔案中通過相對路徑引用的資源都會被配置的路徑所替換
    publicPath: '/assets/'
}

//對於這種結構的專案當然不合適使用這種方法
|- /static
|– /components
|  |– /my-component
|  |  |– index.jsx
|  |  |– index.css
|  |  |– icon.svg
|  |  |– img.png
複製程式碼

當然從我們實際專案的測試效果來看,我只能說這種處理方式並不算是很優秀,僅供參考。

source map dev

在開發環境中,我們比較關注除錯的方便程度,而原始webpack打包後的bundle檔案中可能包含來自多個檔案的內容,對於程式的報錯資訊往往簡單的指向這個bundle檔案:

image.png
而source map是為了幫助我們定位程式出現的錯誤對應的原始碼的位置。使用sourceMap報錯資訊正確的指向了原始碼的錯誤位置。
image.png

//1 使用devtool選項配置,有多個選項可選
module.exports = {
    devtool: 'inline-source-map',
};

//2 使用plugins方式進行更細粒度的配置
module.exports = {
    plugins: [
        new webpack.SourceMapDevToolPlugin({
            filename: '[name].js.map',
            exclude: ['vendor.js']
        })
    ]
};

//在使用uglifyjs-webpack-plugin時 需要開啟sourceMap選項

複製程式碼

devtool文件

UglifyJsPlugin 配合 tree shaking prod

對於js壓縮 在webpack <= 3.x的版本中:

//1 使用 -p(production)標記來壓縮js
//2 使用內建 plugin(webpack.optimize.UglifyJsPlugin)
//3 使用外部引入plugin(uglifyjs-webpack-plugin)
module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
            },
            output: {
                //remove all comments
                comments: false
            }
        }),
    ]
};

複製程式碼

在webpack4中

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
    //1 設定 mode
    mode: ""production
    //2 minimize
    optimization:{
	minimize: true,
        //3 或者指定其他外掛
        minimizer: [
          new UglifyJsPlugin({
            sourceMap: true
          })
        ]
    },
    //3 若需要sourceMap 需要設定 devtool的值
    devtool: 'source-map', 
};

//可選的壓縮外掛
UglifyJSPlugin, ClosureCompilerPlugin
BabelMinifyWebpackPlugin,

複製程式碼

此處需要注意。若是在使用了UglifyJSPlugin且開啟sourceMap後,需要同時給devtool設定值。同樣的若是設定了devtool的值,則UglifyJSPlugin也需要開啟sourceMap。否則不會生成.map的原始碼對應檔案。

在開啟js的壓縮後 我們的tree shaking登場了,tree shaking是什麼?為什麼需要使用?

tree shaking是一個術語,用於描述移除js中未引用的程式碼。 使用它能優化輸出。 未開啟tree shaking的例項:

//tool.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

//index.js
import { square } from "./tool.js"

//最終輸出  在關閉 UglifyJSPlugin外掛後測試結果
複製程式碼

image.png

我們可以看到 cube這個我們並沒有引用的模組也被打包進原始碼了。

使用tree shaking 來優化輸出,在package.json中:

//webpack4
//1 將檔案標記為無副作用 
{
  "name": "web",
  "version": "1.0.0",
  //若是整個專案都無副作用 直接設定為false
  "sideEffects": false
  //若是部分檔案確實有副作用
  "sideEffects": [file_path1, file_path2]
}

//2 開啟js壓縮 使用上述方法開啟即可

//webpack2/3
//1 需要配置 .babelrc modules false
{
  "presets": [
    [
      "env", 
      {
        "modules": false
      }
    ]
  ]
}

//2 開啟js壓縮

複製程式碼

「副作用」的定義是,在匯入時會執行特殊行為的程式碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全域性作用域,並且通常不提供 export。

對於開啟後的壓縮程式碼中,我們搜尋"*"號 只得到一個結果,測試成功。

image.png

最後我們簡單解釋下設定modules false的作用。tree shaking本身是依賴於ES6的靜態匯入,也就是我們常用的import export。ES6模組化中一個檔案能夠輸出多個模組,而我們可以只匯入需要的模組。對比commonjs的動態匯入模組化標準,一個檔案只有一個輸出,因此不難發現,tree shaking在commonjs模組化的系統中是發揮不了作用的。

而modules的意義是啟用將ES6模組語法轉換為另一種模組型別,預設值'commonjs',將該設定為false即不轉化,也就是ES6模組語法,所以在此我們需要將modules設定為false。

modules的取值有 'amd' | 'umd' | 'systemjs' | 'commonjs' | false。 在webpack4中已經可以不用此方法來檢測重複模組了

Split CSS prod

一個web專案中css是關鍵的一環,若沒有額外配置css最終會被打包進入js檔案中,但熟悉瀏覽器渲染的開發者應該會清楚,瀏覽器在渲染頁面時會解析DOM樹和CSS樹,最後將之對應合併呈現渲染好的頁面。將css放在js中引入勢必會延緩css樹的計算。 所以將css從js中分離,打包成單獨的css檔案,然後和js並行載入是我們專案的一個提升點,這樣可以加快介面渲染速度,也可以單獨做快取。

//使用外掛 extract-text-webpack-plugin
{
    test: /\.css$/,
    use: ExtractTextPlugin.extract({
        //用於css未被提取(allChunks: false)
        fallback: "style-loader",
        use: 'css-loader'
    })
}

new ExtractTextPlugin({
    filename: 'common.[chunkhash].css',
    allchunk: true
})

複製程式碼

當然webpack-dev-server是不支援extract-text-webpack-plugin抽離的css熱替換的,所以此外掛不建議再dev環境中使用,如果非要使用可以考慮css-hot-loader。

清理 /dist 資料夾 prod

webpack將打包的檔案放在dist資料夾中,若是使用了hash檔名,則每次檔案變動後重新打包生成的檔名都會不同,這會造成dist目錄越來越混亂,好的做法是每次打包前先清理dist資料夾:

new CleanWebpackPlugin(pathsToClean, cleanOptions)
複製程式碼

在記憶體中編譯 dev

webpack-dev-server大家都不陌生,開發環境必備,webpack內部依賴了webpack-hot-middleware,webpack-dev-middleware兩個外掛。

webpack-dev-middleware提供了在記憶體中編譯功能,它在檔案更改後自動編譯檔案並儲存在記憶體中,具體表現為,重新整理瀏覽器即可看到我們的更改。

webpack-hot-middleware提供了服務端推送功能,通常和webpack-dev-middleware配合使用,當檔案更改並自動編譯完成後,服務端通過SSE(服務端傳送事件)將更改資訊推送到客戶端,客戶端會接收到一個json檔案,其中包含了更改了的檔案的一些資訊,客戶端會根據這些資訊主動向服務端獲取最新的檔案。

若無檔案更改webpack-hot-middleware也會在一定時間間隔後遍歷記憶體檔案檢測是否更改,然後通過事件流的方式向客戶端傳送訊息。

webpack-dev-middleware和webpack-hot-middleware都是express的標準外掛

我相信各位專案中這兩個功能都是已經開啟的我就不再具體說他們的配置了,這裡我們主要說下在node服務端怎麼使用這兩個外掛達到熱更新的目的。

我們以koa為例,如何在koa中開啟熱更新除錯我們的專案呢?

//新建 app.js作為koa服務端入口 app.js

import Koa from "koa";
import views from "koa-views";
import webpack from "webpack";
import webpack_config from "../webpack/webpack.config.js";
import { devMiddleware, hotMiddleware } from 'koa-webpack-middleware'

var app = new Koa()
var compiler = webpack(webpack_config)

app.use(views("./template", {map: {html: "ejs"}}));

app.use(devMiddleware(compiler,{
    publicPath:"/"
}));

app.use(hotMiddleware(compiler))

複製程式碼

koa-webpack-middleware 將express的中介軟體(webpack-dev-middleware和webpack-hot-middleware)進行封裝,將我們koa中介軟體的next方法傳遞到express的第三個引數中進行封裝。

最簡單的配置如上。但這種配置會有一個問題就是重新整理404的問題。

hotMiddleware會在匹配到專案跟路由時直接返回記憶體中的html檔案給客戶端。但是其他的路由如react的路由時,它不會去匹配,最終會返回一個404

//會返回template/index.html 但這時是空的
//也就是沒有匯入js的html
await ctx.render("index");
複製程式碼

解決,當使用者訪問時在webpack編譯輸出的最後階段獲取到檔案資訊,取出獲取到的html檔案寫入template下的index.html檔案,最後返回它,具體操作如下:

compiler.plugin("emit",(comilation,callback) => {
    const assets = comilation.assets;
    let file, data;
    Object.keys(assets).forEach(key => {
        if(key.match(/\.html$/)){
            file = path.resolve(__dirname,"./template/index.html");
            data = assets[key].source();
            fs.writeFileSync(file,data);
        }
    });

    callback();
})
複製程式碼

當然上述方法略顯笨重,且需要理解的東西較多,不太推薦,這裡有一個外掛能解決上述問題 connect-history-api-fallback,大家自己學習下即可。

我的部落格地址

相關文章