moment/moment

Modularize moment.js, make the core as light as possible

srcspider opened this issue Β· 129 comments

I remember when moment was a "lightweight" library, right now at 12kb (gziped no less) for, what it's in most cases, just very very simple date manipulation.

A simple solution to this problem would be to modularize momentjs so at least for environments like browerify, webpack and such a minimal version can be obtained.

eg.

Regular usage can stay the same,

// get everything under the sun
var moment = require('moment');

Modular usage could look like this,

// get core, the core does nothing, no validation, no nothing
// it would only define a internal storage structure and the bare minimum
// methods for getting values out of it: day, month, year, etc (no "format" 
// functions or "fromNow" since you might not even want to use them)
var moment = require('moment/core');

// add plugins for all the stuff you need, if you ever want ALL of them you 
// just include the directory instead (node/browserify/webpack will pick up
// a index.js inside it that would have all the things)
moment.plugin([

    // if you know this is all the parsers you need this is all you add
    require('moment/plugins/parser/yyyy-mm-dd-time'),
    require('moment/plugins/parser/unixtime'),

    require('moment/plugins/validator/yyyy-mm-dd-time'),
    // if we don't use unixtime locally, only on server we dont care for that
    // when it comes to validation

    require('moment/plugins/fromNow'),
    require('moment/plugins/toUnixTime'),

    // with a modular structure we can add 3rd party stuff really easily
    require('moment-phpstyle-format'),
    require('moment-chained-functions-format')

]);

// lock in the configuration so that calling plugin method throw and exception
// this would be irreversible but you can get a unlocked version by calling copy
// this will force people to get a "copy" of the configuration before doing 
// anything stupid -- or help them find the mistake if they add it later
moment.lock();

// you now just include this file where you need it
module.exports = moment;

Let's pretend the above is in something like lib/moment and represents your default boilerplate configuration for it. Obviously you may eventually encounter situations where you need more advanced functions. If we add to the default boilerplate we unfortunately add to every other context that doesn't need it. This is a big bummer if we're trying to keep client size down with things like webpack code splitting.

However, so long as the modular system is capable of being extending any time there's no problem whatsoeve. Ideally, to avoid subtle race condition bugs, we should be able to get a new "advanced" version as a separate instance:

// load boilerplate we defined previously
// we request a copy of the instance so no funny business happens
var moment = require('lib/moment').copy(); 

// we can also create a separate boilerplate if we re-use this a lot
moment.plugin(require('expensive-internationalization-timezone-nonsense'));
moment.lock(); // lock the new copy just in case its passed around

// moment instances should be able to do a quick copy between instances
// to ensure functionality, ie. moment(createdAt) could just swap plugin pointers

Now only the module that actually uses that feature pays the cost. If you use moment in 100 places and only 1 actually needs internationalization just that one place will ever pay for it. This applies to all functions, all the core needs to do is store a plain date object and some getters. Is everything you do just pass unix time from the server? you can just have unixtime parser. Do you only ever validate dates on the server? you can skip on any and all validation, etc.

It's effectively as light as you building your own specialized helpers. And much like projects like gulp, postcss and so on the community can contribute easily to it though easily maintainable chunks.

And in this way, momentjs can, once again, be called "lightweight javascript date library".

right now at 12kb (gziped no less)

Are you using the version from CDN or npm? My size increase is more like 48kb gzipped. #2416

@naartjie

You'll want to use webpack's ignore plugin (sorry cant recall name of the top of my head) to exclude locale information. That should bring it down to something more reasonable.

48kb gzip is indeed pretty crazy, my largest entry point (react + various util functions + application code for full rendering) sits at around 50kb gzip. I've switched to just using unix timestamps and completely custom functions to process those into a format as needed... a bit sad, would love to use a date manipulation library but just can't take a +100% initial load javascript bump. It's obviously even worse on subsequent incremental loads (caused by require.ensure) since those average at 10kb gzip as it's mostly application specific code, so should initial load avoid dates but one of those require dates then that's a 6x size increase.

Thanks for the rundown @srcspider, I have read about IgnorePlugin before, but I didn't click that it was applicable for these kinds of situations, so it helps to match theory and practical together πŸ‘

Does anyone need a library that gets bigger and bigger?

Don't you want to edit that line out? It might seem a little too brash, where as I think your general tone of the issue is very helpful, and you have a valid point. Plus you seem to know how this should all go down, I think a PR would go down well, I hear they're looking for help. ;-)

I think it would be great to have the option to require a version with or without locales, that would be good at least for a start, and probably not too massive a refactor. I could have a stab at a PR. What say you @srcspider, do you think that would be useful to anyone else?

I know it's a πŸ‘ from me, but I'm not sure if there would be any other takers out there.

@naartjie not to be the bearer of bad news but would probably recommend waiting on one of the owners to give their thoughts on this before putting any considerable effort in. No point in pushing only to be shot down (or never approved). However if you need it yourself right now, by all means.

Plus you seem to know how this should all go down, I think a PR would go down well, I hear they're looking for help. ;-)

Sadly I'm very much on the fence on ES6. It doesn't integrate with my current workflows, and can't see any advantage to the various recommendations of ES6 workflows I've encountered (and by "no advantage" what I mean is they're far far inferior in resulting size, architecture and just a lot more unnecessarily complex in general).

It's hard to get exited to contributing to a "Port to ES6" when it's, overall, just a worse thing (for me).

The port to es6 is supposed to benefit development, not users. There are packaged versions of moment, with and without locales, uploaded to npmjs.

About making a small core that is extensible - that is going to be a big step back in usability for most users. If you feel like it patch the src/moment to include what you want and transpile.

I'm closing this for now. If a moment without locales is not bundled properly feel free to reopen.

There are packaged versions of moment, with and without locales, uploaded to npmjs

@ichernev I tried looking, and all I found was moment and moment-timezone, could you point me to the version of moment without locales in npmjs.

I actually asked for this in #2416. It feels like using the IgnorePlugin is a hack/workaround, if there is already a packaged version without locales.

Thanks.

Aaah, I see. Npmjs is historically used for server side, where you don't care about size. But for other build tools that go on top of it, I guess we shall add another target moment-core or something like this.

That would be great, thanks @ichernev. It will make integrating with tools like webpack a breeze (without needing the likes of IgnorePlugin).

I see you've reopened this one, so I'm going to close #2416 again ;-)

@ichernev I use npm for both frontend and backend, and I think this has become much more common. I'm also a bit concerned with loading in a giant library just for the use of a couple nice date manipulation apis. +1 for a lean-core via npm, and an easy way to optionally add locales / peripheral functionality with the default being exclusion (unless explicitly included).

One thing I think would be great to be able to do, is load a single locale as needed via webpack's ensure functionality - so you only load locale information when the user actually needs it (eg loads the page in a particular locale or switches their configured locale)

+1 definitely

+1

would love this as well.

+1

One of the first (and best) optimizations we did was remove moment.js from our client side app. I would love to use moment.js more frequently but the additional page weight just isn't worth it for the basic date manipulation we do.

So, I'm a big +1 on this as well.

@jhubert I'm actually starting to consider that. What did you replace moment with? It's one of the largest things in my app.

@rey-wright I was really only using it for formatting, so I just wrote a few functions that formatted the dates exactly as I needed them and used them throughout the site. It's nothing elegant, but it's extremely fast and light.

@jhubert yeah we were doing that but... I really wish moment would be setup better...but now it's like Moment is one of the biggest pieces of our app...

So yeah we might eventually go back to this method as well. Thanks for the insight.

+1. Moment.js looks great, but the 12kb makes it not worth the include (that's bigger than the framework we use!)

tests.js file could be removed from the npm package. This file amounts to 63% of the total npm package size. A dedicated development npm package could could include this tests.js file.

For desktop applications (electron) using npm packages using moment, moment contributes significantly to the final application size (moment folder is duplicated in nested nod_modules dir).

@DerekDomino The point of modularizing moment.js is for two reasons:

  • Less code for browsers to download
  • Easier to maintain many smaller packages than one large one

Removing a tests file doesn't accomplish either of those goals, and in fact doing what you suggest would make it harder to develop moment. And if you're for some reason having browsers forced to download the tests.js code, something else is going wrong.

@fresheneesz While yes, the suggestion doesn't fix the main problem, why would excluding a test file from the npm package make it harder to develop moment? It isn't the same as removing tests from the repository, and, generally, test code is not meant to be packaged.

Oh, just the npm package? I guess it wouldn't then. Still don't think its a road worth traveling.

πŸ‘

@ichernev has there been any work done on this?

niksy commented

I’ve also been looking for this, but in the meantime you can try https://date-fns.org/. It doesn’t have full Moment.js functionality, but it covers a lot of standard use cases.

I like the solution lodash uses, if you look it's branches you can see they have a special x.y.z-npm branch, and inside they have all the utilities separated in their own files in the root directory. This allows you to just import { whatever } from 'lodash' without importing the whole library.

Edit: Sorry, for Lodash it would be import whatever from 'lodash/whatever'

This allows you to just import { whatever } from 'lodash' without importing the whole library.

@Zequez If I use webpack for front-end, still including all the libs.

@haoxins Sorry, yes, there I corrected it with the correct syntax, you have to import from lodash/functionName. Not the ideal, as you need a statement for each function. But modular enough I guess.

Definitely an improvement on the current workaround: i.e. exclusions in
webpack.

On Tue, Feb 2, 2016 at 5:25 PM Ezequiel Schwartzman <
notifications@github.com> wrote:

@haoxins https://github.com/haoxins Sorry, yes, there I corrected it
with the correct syntax, you have to import from lodash/functionName. Not
the ideal, as you need a statement for each function. But modular enough I
guess.

β€”
Reply to this email directly or view it on GitHub
#2373 (comment).

@niksy awesome suggestion, I'm going to take a look at it today, and I'll try to come back here and comment once I have a good grasp on it.

fxck commented

any update on this?

+1 I'm very much in support of a lodash-style solution. Users unconcerned with bundle sizes can still just import moment from 'moment', but I'd like to be able to do something like import moment from 'moment/core'.

The webpack solution is working for me, but it's still a pretty big bundle for what I need it for (simple date formatting/calculation).

I feel there are many dev's who avoid moment for its size, there are alternatives coming in at less then 10%

@andrewmclagan there's worse then that. How would you feel if a library you wanted to use pulled moment in when you weren't looking? ;)

haha don't worry thats exactly whats happening to me right now...

@andrewmclagan: Can you recommend a good datetime library that comes in at less than 10% of Moment's size?

@mattgrande : https://github.com/date-fns/date-fns takes a lodash-like approach to the modularity problem

Thanks, I'll check it out.

Yeah also using date fns

So checking into date-fns, moment.min.js (the current version that includes all comments for some reason) is 57.3KB; The last version without comments was 45.6. Meanwhile, date-fns is 56.9 KB, only 0.4KB difference.

selection_164
Moment is the most heavyweight library on my stack!

@anaibol how are you building that? Does it include locale files?
It's really odd to me that one person is reporting 135k and another 57.3. Something else is going on here.

@mattgrande I think the point of date-fns is to not bundle the whole library. Using tools like webpack or rollup with tree shaking you can only bundle the modules you're using in the application. This isn't possible with the current moment.js and that's exactly why this issue exists. So this comparison isn't valid to me.

@okonet using webpack@2 (with tree shaking) also bundle all modules in moment.js.

@maggiepint: building moment@2.13.0 with webpack@2 and new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /fr/) and using it with import moment from 'moment'

@newraina I know it does. This issue exists for that reason. Am I missing something you're trying to say?

@anaibol I use this to exclude locales in webpack:

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) // Ignore all optional deps of moment.js

@mattgrande

So checking into date-fns, moment.min.js (the current version that includes all comments for some reason) is 57.3KB; The last version without comments was 45.6. Meanwhile, date-fns is 56.9 KB, only 0.4KB difference.

This was already pointed out by @okonet but to put it into perspective, I resorted to just writing my own functions to achieve the same thing and making sure everything works only with timestamps and the overhead of date related functions when used in a webpack build is measured in Bytes not Kilobytes for most bundles (use require.ensure a lot so it's all incrementally loaded too).

Don't currently use it since already wrote my own functions, but date-fns does the same thing, here's the example they give right on their front page:

var isToday = require('date-fns/is_today')
isToday(new Date())
//=> true

Thanks @okonet, that worked pretty well for me. Went from 420kb to 249kb (171kb diff)

Using @okonet's advice is good enough to get by for now. I was able to reduce my build size by about 140kb (953kB -> 807kB). Thanks!

Yes please

Any news on this? Is the core team considering something along the lines of lodash-style way of importing just the stuff you need?

We're thinking about it. Its coming after immutability, maybe :)

screen shot 2016-08-25 at 11 53 29 am

You guys are complaining about 145Kb... :(

Yes we are.

@framerate

Are those the sizes on disk? On network everything should be gziped. The disk size does play out, but it's a consideration of "how much javascript browser has to load before first render" (smaller is better).

Bellow show maximum gzip; depending on your server settings it might look very different.

libs.js is the commutative shared libraries as generated by webpack
(actual disk size is 267K; mostly react -- babel adds some bloat to everything too)

# gzip-size libs.js | pretty-bytes
75.82 kB

Random common place page (signup.js) entry point.

# gzip-size signup.js | pretty-bytes
4.95 kB

Between pages, partial js download happens (require.ensure). The following is the largest one,

# gzip-size partial.4.e4d6ff3506d1ded07d0f.js | pretty-bytes
4.96 kB

moment.min.js

# curl -L -o moment.min.js http://momentjs.com/downloads/moment.min.js
# gzip-size moment.min.js | pretty-bytes
20.22 kB

4x page, 4x partials, and 1/4 of ALL other dependencies combined in size? The functionality moment provides just doesn't justify it. The library should be practically invisible when looking at the sizes.

Lodash is actually very similar. If I were to include the entire thing it actually looks like this,

# curl -L -o lodash.min.js https://cdn.jsdelivr.net/lodash/4.15.0/lodash.min.js
# gzip-size lodash.min.js | pretty-bytes
23.45 kB

But I explicitly require the functions I need so it doesn't even show up at all. Because really even if your library has a ton of functionality, nobody actually uses everything on every single page.

@srcspider Yeah, but this is using via npm + webpack with locales (other than en) stripped out using the techniques/hacks in this thread.

(to be clear, the screenshot was NOT stripping out the locales. After adding stuff from the thread I'm down to about 145kb)

@framerate sorry for an off-topic question, but what's the name of that module you use to analyze sizes of packages in the build? That fancy table you shown - I am now working on build optimization and this would be really handy to me. Thanks!

@vojtatranta Also check out https://github.com/robertknight/webpack-bundle-size-analyzer it is pretty easy to use just

webpack --json | webpack-bundle-size-analyzer

and it spits the details in the console.

@jeffbski yeah I am using it but sad thing is that it does not show the size packages in minified build and it does not even show how the package will be deduped

@vojtatranta check this one https://github.com/th0r/webpack-bundle-analyzer Nice visualisation, also showing min and gzip sizes

In our app:

webpack --json | webpack-bundle-size-analyzer

Before ignoring locales:

> cat profile.json | webpack-bundle-size-analyzer
moment: 466.96 KB (20.5%)
jquery: 260.93 KB (11.5%)
iconv-lite: 199.46 KB (8.76%)
moment-timezone: 188.54 KB (8.28%)

after ignoring locales:

> cat profile.json | webpack-bundle-size-analyzer
jquery: 260.93 KB (13.4%)
iconv-lite: 199.46 KB (10.2%)
moment-timezone: 188.54 KB (9.68%)
lodash: 141.27 KB (7.25%)
moment: 137.34 KB (7.05%)

Note: webpack-bundle-size-analyzer returns sizes before any minification/uglification etc. is applied.

However, we can look at the actual output size:

> NODE_ENV=production npm run webpack

... with locales ...
app.js  1.08 MB

... no locales ...
app.js  925 kB

Locales do add a very significant chunk to the overall size.

What about this issue?

I reverted to using the extra-light dateformat because of all the unneeded clutter this library bundles...

@caesarsol look into date-fns. I was using moment, then the library you cited, then transitioned to this one... using it in production, works well. https://date-fns.org/

Use:

  • new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), in your webpack config, or:
  • lighter alternatives like date-fns or dateformat

If you need i18n/l10n though:

  • new webpack.IgnorePlugin() with anything that's not your locales, or:
  • (how we do ended up doing it) load required locale in a <script> tag separately (it may require tweaking locale code etc.)

this module exposes moment without the locales https://github.com/ksloan/moment-mini

Switching to moment-mini was easy and shaved 226kB off the build size

pke commented

momenjs even without the locales still comes in at 140kb atm. For simple date formatting, that's way to much. Switching to something else now.

@pke - Whats your alternative?

pke commented

Indeed date-fns is the way to go. Remember though, moment started as 4.3kb package too back in the days.

I think that if moment.js was split into a file-per-function like lodash, and then with the help of something like the lodash webpack plugin, it could be tree shaken so only the used functions remain.

pke commented

True, but date-fns already provides all that

pke commented

True, but date-fns already provides all that

@pke it doesn't matter what size date-fns ends up being, as long as you can specifically require certain things.

The current ES6 export is not really an ES6 export.
See my PR showing the ES6 way of exporting:
#3905
Using ./src is possible when no ES6 features are used (default arguments, const, spreads, etc), otherwise it would be ./es6-build folder.

Anybody who wants to actually make a dent at this issue (excluding the webpack, all-locales-together, 300k issue), should reorganize the code and put the stuff that s/he thinks is splittable in folder(s), and show what the gains will be.

Here's currently how much each file contributes http://iskren.info/moment-size.html

Keep in mind that concepts are intertwined, so for example week support, locale support, and generic parsing support (I list these because they're IMHO some of the biggest things in moment) sometimes all go in the same file, and one can not really split them, because they're not orthogonal, so out of those 3, maybe only one can be split out (driving pieces of the other 2 with it). My inner feeling tells me splitting each of those will cut max 20% off the size, and you can do it only once, so the gains will not be huge.

pke commented

Sounds all very fine. Someone might give it a try. I've moved over to date-fns and left momentjs behind. Got to finish some production app, and need to squeeze every byte. Good luck!

Until I had a deep dive into the code in the last couple of days, I would have been 100% +1 on this. But now I think that the libary is not structured to support plugin functionality in any meaningful way and would need a complete redesign (something like date-fns) to support it.

The problem lies in the fact that the library treats its namespace like an Object. This is a problem, because a plugin effectively has to "extend" the namespace in order to contribute a function to it. However the library also assumes that it's a singleton, so only one of these "subclasses" can really be instantiated at the one time. It's slightly more nuanced than that (the reason is that there is no way to introduce "static" plugins -- ones which (apart from their dependency graph) are not sensitive to the order in which they're loaded), but I think the subclassing metaphor is the easiest way to describe it.

I tried decoupling duration from the rest of the library, since it seemed like the easiest thing to decouple (even if it wasn't the biggest feature). It sort of worked (or at least it looked promising), but then I tried decoupling locale, and I realised that a plugin system would never work with the code structured the way it is.

It's certainly possible to do something like rxjs 5 did for when they rewrote their library to be more modular, but it would require a lot of breaking changes and it would take a lot of developer hours.

And I think the webpack people here who are having problems with the 300kb bundle size of moment would agree that we need 3.0 quicker than that in order to make the breaking changes necessary in order to fix that problem (see my rants at #4025).

There are also plenty of refactorings which could be made for 3.0 (or even 2.19/2.20) which would reduce the bundled size without requiring functionality to be split out into separate plugins...

@ovangle, thanks for taking time to truly understand this issue. As we've pointed out on this thread, this ask is extremely non-trivial for this code - probably nothing less than a couple weeks of dev time from someone on the core team.

For the webpack bundle size specifically, I think we would be reasonably able to take your changes described in the other issue. Headed over there to discuss.

I use the JS date functions from the Quasar framework. It can do treeshaking easily like so:

// we import all of `date`
import { date } from 'quasar'
// destructuring to keep only what is needed
const { addToDate } = date

Is this not yet possible with momentjs?

pke commented

Maybe moment.js had its moment and its time to head to something new more compatible with webpack and small bundle sizes aka date-fns. I am out of here. Good luck :)

@mesqueeb

There are also plenty of refactorings which could be made for 3.0 (or even 2.19/2.20) which would reduce the bundled size without requiring functionality to be split out into separate plugins...

Making the moment namespace itself static and allowing tree shaking to do the work of removing unused functionality than completely decoupling the code in the short term... then at least some of moment would a valid target for tree shaking.

I've had good success transitioning to date-fns. I cut my bundle size substantially because I was only using the momentjs format function, of which date-fns has pretty much a drop-in replacement.

...damn
image

+1 too big

@petermikitsh thanks for the suggestion. Switched as well and it works like a charm!

This is probably not the answer to prayers those on this thread wanted - however we do have what I will call a 'moment labs' project going on that is significantly smaller. It is not a compatible API, but it is properly modularized and comes from @icambron of the moment team. I invite interested parties to try it out and give feedback: http://isaaccambron.com/luxon/docs/

Thanks to all, I came across the same situation trying to bundle moment with webpack and could build on all proposals a quite acceptable way at least for me:

  1. Use uncrompressed sources of momentjs.
  2. Bundle only required locales.

Basicly it is just combining these ideas:

  1. https://webpack.js.org/configuration/resolve/#resolve-alias
  2. https://webpack.js.org/plugins/context-replacement-plugin/#content-callback

These are the relevant parts in my webpack.config.js.

const path = require("path");
const webpack = require("webpack");

module.exports = () => {
    return {
        // ...
        resolve: {
            // Use src Moment.js to be optimized and packed.
            alias: {
                moment$: "moment/src/moment",
            },
        },
        // ...
        plugins: [
            // Switch context for Moment.js locales.
            new webpack.ContextReplacementPlugin(/^\.\/locale$/, context => {
                // Don't touch anything else then "moment".
                if (!/\/moment\//.test(context.context)) {
                    return;
                }
                // Working with "moment/src/moment" instead of "moment" requires
                // redirecting "./locale" back to "../../locale".
                Object.assign(context, {
                    // Want all locales, enable this line.
                    // regExp: /^\.\/\w+/,
                    // Just use some locales, enable this line.
                    // regExp: /de|fr|hu/,
                    // Don't use any locales except default "en".
                    regExp: undefined,
                    request: "../../locale",
                });
            }),
            // ...
        ],
        // ...
    };
};

+1 to tackle this bundle size problem and ES6 compatibility otherwise moment is already a dead library.

I'm not 100% sure how to explain the context in which this is an issue, but it may help someone else. Right now I have a package which depends on moment.js which has a dependency on a different package that depends on moment.js.

Both of these packages are built with webpack and include this line:

plugins: [
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]

However it looks like what's being consumed is the result of moment.js's own Grunt task, which outputs locales to a module defined as moment/min/locales, which won't match the regexp above. As a result, the locales still get included in the webpack output.

You will need something like this instead, to match the module definition moment/min/locales instead of the local file path ./moment/locale:

externals: {
    'moment/min/locales': '{}'
},

This will stub out this dependency as an empty object. Viola!

MickL commented

I love moment.js but yes 130kb without locales is way too much for using 1-2 functions out of it. Tree shaking is a MUST HAVE as of today. Will move to date-fns, too.

screen shot 2018-03-01 at 18 46 02

This is what I see after running everything through UglifyJS

πŸ‘ Thanks to this thread for introducing me to date-fns! Moment.js is out, modularity is in!

oh yeah, i found a solution:

    new webpack.ContextReplacementPlugin(
      /moment[\/\\]locale$/,
      /en|de|fr|es|pl|ua|ru/
    ),

Thanks @revmischa - this works for Moment and is generally a good alternative to Webpack's IgnorePlugin which I could never get working. Cheers dude πŸ˜„

I want to be able to only use moment.js for duration? It will be so awesome if I can only require/import the duration module from moment. In web pack after following the above recommendations I still have to take ~60KB of minified moment.js just for validating the duration format.

Can we please modularize moment? If that is not possible then is there another way to tell web pack to not take the whole library but specific functions and bring source code that only touches those functions.