如何使用 webpack 優化 moment.js

語言: CN / TW / HK

(1)清洗moment語言環境檔案

預設情況下,當您編寫var moment = require('moment')程式碼並使用 webpack 打包時,捆綁檔案的大小會變得很重,因為webpack 會捆綁所有Moment.js 所有語言環境檔案(在 Moment.js 2.18.1 中,壓縮後的 KB160)。

要去除不必要的語言環境並僅捆綁使用的語言環境,請新增moment-locales-webpack-plugin

// webpack.config.js
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');

module.exports = {
    plugins: [
        // To strip all locales except “en”
        new MomentLocalesPlugin(),

        // Or: To strip all locales except “en”, “es-us” and “ru”
        // (“en” is built into Moment and can’t be removed)
        new MomentLocalesPlugin({
            localesToKeep: ['es-us', 'ru'],
        }),
    ],
};

為了優化大小,還可以使用兩個 webpack 外掛傳送門

  1. IgnorePlugin
  2. ContextReplacementPlugin
IgnorePlugin

您可以使用IgnorePlugin.

const webpack = require('webpack');
module.exports = {
  //...
  plugins: [
    // Ignore all locale files of moment.js
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],
};

而且您仍然可以在程式碼中載入一些語言環境。

const moment = require('moment');
require('moment/locale/ja');

moment.locale('ja');
...

Create React AppNext.js使用這個解決方案。

ContextReplacementPlugin

如果要在 webpack 配置檔案中指定包含語言環境檔案,可以使用ContextReplacementPlugin.

const webpack = require('webpack');
module.exports = {
  //...
  plugins: [
    // load `moment/locale/ja.js` and `moment/locale/it.js`
    new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /ja|it/),
  ],
};

在這種情況下,您不需要在程式碼中載入語言環境檔案。

const moment = require('moment');
moment.locale('ja');
...
測量
  • webpack: v3.10.0
  • moment.js: v2.20.1
File sizeGzipped
Default266 kB69 kB
w/ IgnorePlugin68.1 kB22.6 kB
w/ ContextReplacementPlugin68.3 kB22.6 kB

How to optimize moment.js with webpack

(2)unused code

在我們的實際專案中,moment被整合在bricks基礎元件庫中,bricks元件庫在構建時已經對語言環境檔案做了清洗處理,但是在業務程式碼編寫過程中實際使用moment的場景特別少,只用到了幾個零碎的api,卻要承擔引入整個moment.js構建到產物中的代價。這時你可能要問到我們不是有tree shaking嗎?它可以幫我們自動清除無效程式碼,但事與願違,moment高度基於OOP API(面向原型鏈程式設計),所有api都掛載到原型鏈上,導致無法使用Webpack新引入的Tree-shaking程式碼優化技術,無法識別哪些程式碼是dead code

Moment還存在如下一些問題
  • 它高度基於OOP API,這使得它無法使用 tree-shaking,從而導致巨大的包大小和效能問題;
  • 它的可變性將導致一些時刻計算問題;
  • 複雜的OOP API使得Moment可變性問題更加嚴重,這兒有個例子https://github.com/moment/mom...
  • Moment效能一般,由於複雜的API使得Moment與原生Date相比有著巨大的效能開銷;

Moment.js擁有一些問題

Moment可變性

當我開始使用 moment 時,我假設它遵循 FP 原則,並且每次呼叫函式時都會返回相同的值:

var now = moment();
var yesterday = now.subtract(1, 'days');
var dayBeforeYesterday = now.subtract(2, 'days');

當然,我沒有得到我期望的結果,這讓我措手不及。

考慮這個虛擬碼,我期望如下程式碼行為方式:

var now = now;
var yesterday = now - 1day;
var dayBeforeYesterday = now - 2days;

但事與願違,它最終像這樣工作,這讓我感覺很奇怪:

var now = now;
var yesterday = now = now - 1day;
var dayBeforeYesterday = now = now - 2days;

Moment物件的可變性使得我只能小心翼翼使用.clone()

var now = moment();
var yesterday = now.clone().subtract(1, 'days');
var dayBeforeYesterday = now.clone().subtract(2, 'days');

Moment使用過程很容易出現這些細微的錯誤,我認為 FP 原則有助於最大限度地減少類似錯誤。

參考:
怎麼使moment物件不可變
moment物件可變性造成的問題
如何解決 moment.js 中的可變性?

替換方案

如果您沒有使用時區,而只使用了moment.js中的一些簡單函式,這會導致你的應用程式被引入了很多沒使用的方法,這是極其浪費效能和記憶體的。 在這裡推薦使用dayjs, dayjs體積非常小,超小的壓縮體積,僅僅有2kb左右,所有更改Day.js物件的API操作都將返回一個新的例項(不可變性),和Moment.js有著相同的API和模式,因此很容易從moment平滑過渡到day.jsdate-fns支援Tree-shaking程式碼優化技術,提供友好的 functional programming (FP) 函式(都是純函式),支援函式柯里化,支援typescript,它的不可變效能很好的彌補moment帶來的問題,因此它很適合與React,Sinon.jswebpack等好基友一起使用。

moment.jsday.jsdate-fns簡單比較:

名字大小(gzip)支援Tree-shaking名氣api方法數模式時區支援支援的語言數
Moment.js329K(69.6K)No38kOO非常好(moment-timezone)123
date-fns78.4k(13.4k) without tree-shakingYes13kFunctional還不支援32
dayjs6.5k(2.6k) without pluginsNo14kOO還不支援23

Moment.js的替換方案

專案實踐

首先排查應用程式依賴Moment情況,發現Moment整合在bricks基礎元件庫中,只有日期元件使用了moment,當前應用程式沒有使用日期元件,而且也沒有其他依賴Moment的模組被安裝,業務程式碼使用Moment的場景也很少,僅使用calendarformat兩個api,程式對moment依賴程度極低,因此完全可以將Moment從業務程式碼中移出,引入更加零輕量級day.js,或者支援Tree-Shakingdate-fns,在或者原生實現format方法。

綜合考慮,鑑於date-fns支援FPTree-Shaking,具有不可變性,跟React狀態不可變性、FP原則的思想完美吻合,選擇使用date-fns替換掉bricks整合的moment模組,替換momentformatcalendar方法

(1)因為bricks集成了moment,需要先排除其被打包到最終產物中

// config-overrides.js

/** 清除bricks元件整合的moment,不讓其參與打包 */
const eliminateMomentOfBrs = (config) => {
  config.plugins.push(new webpack.IgnorePlugin({ resourceRegExp: /moment/ }));
}

module.exports = function override(config, env) {
    eliminateMomentOfBrs(config);
}

(2)替換業務程式碼中momentformatcalendar方法

// moment.js
moment(time).format('MM月DD日'); // 09月02日

// date-fns
import { format } from 'date-fns';
format(time, 'MM月dd日'); // 09月02日

// moment.js
moment(time).calendar(null, {
  sameDay: '[今日]HH:mm',
  nextDay: '[明日]HH:mm',
  nextWeek: 'M月D日 HH:mm',
  lastDay: 'M月D日 HH:mm',
  lastWeek: 'M月D日 HH:mm',
  sameElse: 'M月D日 HH:mm',
}); // // 8月27日 09:23

// date-fns
import { format, formatRelative } from "date-fns";
import { zhCN } from "date-fns/esm/locale";

const formatRelativeLocale = {
  lastWeek: "M月d日 HH:mm",
  yesterday: "M月d日 HH:mm",
  today: "[今日]HH:mm",
  tomorrow: "[明日]HH:mm",
  nextWeek: "M月d日 HH:mm",
  other: "M月d日 HH:mm"
};

const locale = {
  ...zhCN,
  formatRelative: (token) => formatRelativeLocale[token]
};

formatRelative(time, new Date(), { locale }); // 8月27日 09:23

如果你正在使用 ESLint, 你可以安裝一個外掛plugin 來幫助你識別程式碼庫中你沒有(可能不需要)Moment.js的地方,防止同學不經意安裝引入moment

安裝這個外掛...

npm install --save-dev eslint-plugin-you-dont-need-momentjs

...然後更新你的配置

"extends" : ["plugin:you-dont-need-momentjs/recommended"],

優化前後對比:

移出moment使得build\static\js\2.7fde9c2a.chunk.js體積減少17.52KB,但新增date-fns使得build\static\js\3.4a62a5b9.chunk.js增加8.76KB,整體體積減少接近10KB,效果不是很明顯

對視覺化樹狀圖進一步分析發現程式中已經引入了dayjs,但程式並沒有單獨安裝它,執行npm list dayjs,發現只有封裝大額業務元件庫@casstime/mall-components使用了它

基於以上觀察,引入date-fns看來是沒有必要的,直接使用dayjs即可,長久考慮,統一換成date-fns比較好。

優化前後對比:

再次調整優化後幹掉了整個moment模組,並且沒有新增其他模組,和業務元件@casstime/mall-components共用dayjs,產物體積整體減少17.5KB(大於上次優化的10KB

Day.js

本文來源:如何使用 webpack 優化 moment.js