如何使用 webpack 优化 moment.js

(1)清洗moment语言环境文件

默认情况下,当您编写var moment = require('moment')代码并使用 webpack 打包时,捆绑文件的大小会变得很重,因为webpack 会捆绑所有Moment.js 所有语言环境文件(在 Moment.js 2.18.1 中,压缩后的 KB160)。

img

要去除不必要的语言环境并仅捆绑使用的语言环境,请添加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相比有着巨大的性能开销;

image-20220901174103823.png

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方法。

image-20220902104844763.png

image-20220901111151675.png

image-20220901111237045.png

综合考虑,鉴于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,效果不是很明显

image-20220902114219356.png

image-20220902114610847.png

对可视化树状图进一步分析发现程序中已经引入了dayjs,但程序并没有单独安装它,执行npm list dayjs,发现只有封装大额业务组件库@casstime/mall-components使用了它

image-20220902134148411.png

基于以上观察,引入date-fns看来是没有必要的,直接使用dayjs即可,长久考虑,统一换成date-fns比较好。

优化前后对比:

再次调整优化后干掉了整个moment模块,并且没有添加其他模块,和业务组件@casstime/mall-components共用dayjs,产物体积整体减少17.5KB(大于上次优化的10KB

image-20220902162047945.png

image-20220902162301610.png

Day.js

avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1.7k 声望
4.4k 粉丝
0 条评论
推荐阅读
alicdn边缘节点不稳定导致页面崩溃问题
某工作日,线上某用户向客服专员反馈没法正常访问“查看报价页面”,页面内容没有呈现。客服专员收到反馈后,将问题转交给SRE处理。很奇怪的是,SRE访问生产环境“查看报价页面”显示正常,为了进一步分析定位问题,S...

记得要微笑阅读 703

耗时一年半才出第一版,这个工具会一统前端么?
大家好,我卡颂。前端领域从不缺少热点,基本每过半年,就会出现新的工具。在这样快节奏的浪潮中,有个工具却显得格格不入,他就是Rome。从名字中我们就能窥探出一丝端倪,看看别的工具:vite(法语中快的的意思...

卡颂3阅读 1.2k评论 1

封面图
Turbopack 发布后的各方反应:Vite/Webpack/围观群众
上周整理了一下 Turbopack 发布后的各方反应,以便让自己和各位同学下一步做决策的时候能有所参考。接着忙碌的一周过去,我发现博客这周还没更,于是赶紧来补一下。

Meathill6阅读 805评论 1

封面图
极致编译速度,一文搞定webpack5升级
本文作者:xiongxiao01在尝试升级 webpack5 之前,建议大家尽量先把官方文档通读一遍,可以少走很多弯路,本文是在结合具体业务场景后,对官方文档的归纳和补充。

云音乐技术团队4阅读 676

封面图
想弄懂Babel?你必须得先弄清楚这几个包
相信很多人都知道Babel,知道它是用来编译ES6+的东西。但是再深入一点,大家都清楚我们平时项目中Babel用到的那些包作用是什么吗?为什么要用那几个包?

limingcan1阅读 489

封面图
前端脚手架开发入门
脚手架是一个通用开发工具,之前自己写一下原生web工程时总是需要重复搭建开发环境、重复写几乎相同的配置文件。为了避免每次的重复工作,统一开发环境、规范,于是想到自己写一个脚手架用用,顺便记录一下。

coderLeo1阅读 422

封面图
前端微服务跨域配置解决办法,devServer为例
前言Nginx: 在上一篇我提到的跨域配置是正式上线的时候使用nginx做为配置的参考。Webpack: 而我们更多的时候是在开发阶段就需要通过跨域进行联合开发各个子应用部分功能DevServer配置解决跨域子应用静态资源跨域...

smallStone1阅读 985评论 4

avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1.7k 声望
4.4k 粉丝
宣传栏