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

Open
srcspider opened this Issue May 13, 2015 · 64 comments

Projects

None yet
@srcspider

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".

@naartjie

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

@srcspider

@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.

@naartjie

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. ;-)

@eliseumds eliseumds referenced this issue in Hacker0x01/react-datepicker Jun 15, 2015
Closed

Dist scripts are really heavyweight #123

@naartjie

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.

@srcspider

@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).

@ichernev
Contributor

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.

@ichernev ichernev closed this Jun 25, 2015
@naartjie

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.

@ichernev
Contributor

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.

@ichernev ichernev reopened this Jun 29, 2015
@naartjie

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 ;-)

@fresheneesz

@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)

@reywright

+1 definitely

@jeffbski
jeffbski commented Aug 4, 2015

+1

@seethroughtrees

would love this as well.

@dbrugne
dbrugne commented Sep 3, 2015

+1

@jhubert
jhubert commented Sep 21, 2015

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.

@reywright

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

@jhubert
jhubert commented Sep 25, 2015

@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.

@reywright

@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.

@mindeavor

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

@mj1856 mj1856 added the Enhancement label Oct 24, 2015
@DerekDomino

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).

@fresheneesz

@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.

@vsimonian

@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.

@fresheneesz

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

@iagopiimenta

👍

@aweary
aweary commented Jan 25, 2016

@ichernev has there been any work done on this?

@niksy
niksy commented Jan 25, 2016

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.

@Zequez
Zequez commented Feb 2, 2016

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'

@haoxins
haoxins commented Feb 2, 2016

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.

@Zequez
Zequez commented Feb 2, 2016

@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.

@naartjie
naartjie commented Feb 2, 2016

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).

@mj1856 mj1856 referenced this issue Feb 17, 2016
Closed

moment and es6 #2968

@mj1856 mj1856 added this to the 3.0 milestone Feb 17, 2016
@darrint darrint referenced this issue in DirectEmployers/MyJobs Feb 26, 2016
Closed

[PD-1952] Restore Report Naming #2143

@reywright

@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
fxck commented May 18, 2016

any update on this?

@joshwcomeau

+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).

@andrewmclagan

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

@srcspider

@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? ;)

@andrewmclagan

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

@mattgrande
Contributor

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

@avindra
avindra commented Jul 5, 2016

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

@mattgrande
Contributor

Thanks, I'll check it out.

@andrewmclagan

Yeah also using date fns

@mattgrande
Contributor

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.

@clauderic clauderic referenced this issue in clauderic/react-infinite-calendar Jul 17, 2016
Closed

Moment Dependency Concern #31

@anaibol
anaibol commented Jul 17, 2016 edited

selection_164
Moment is the most heavyweight library on my stack!

@maggiepint
Member

@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.

@okonet
okonet commented Jul 18, 2016

@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.

@newraina

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

@anaibol
anaibol commented Jul 18, 2016 edited

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

@okonet
okonet commented Jul 18, 2016

@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
@srcspider
srcspider commented Jul 18, 2016 edited

@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
@davidcalhoun

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

@petermikitsh

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!

@akinnee-gl

Yes please

@Haraldson

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

@ichernev
Contributor

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

@framerate

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

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

@akinnee-gl

Yes we are.

@srcspider
srcspider commented Aug 26, 2016 edited

@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.

@framerate

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

@framerate

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

@vojtatranta

@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!

@framerate

@vojtatranta No Problem! It's webpack-dashboard

https://github.com/FormidableLabs/webpack-dashboard

@jeffbski

@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.

@vojtatranta

@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

@ilionic
ilionic commented Dec 25, 2016

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

@dmitriid
dmitriid commented Jan 2, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment