Hack Your Way to a Smaller SPA

As Scott O'Brien always says, JavaScript is bad.  In all seriousness, our front-ends wind up bloated, really quickly, which leads to poor performance.   We mostly aim to keep the package sizes down to get to the first meaningful paint as fast as possible, but less JavaScript also means less time parsing it.  With my latest project, I opted for a React stack, but I am doing my best to keep dependencies down (sorry, not sorry Redux!), but some things are too useful to avoid.  A few months in, and all that exists so far is React, React Router, Formik, Selectize, and class-validator.  The later was proving to be a problem.

We are using class-validator with Formik, to write some expressive, self-validating, DTOs.  It's really nice, and we are using it on the back-end, so a few lines of code, and some copy-pasta gets us a form up and running, really quickly.  But I was curious.  I knew Create React App bundles don't start off small, and I was being pretty careful, so I ran source-map-explorer on it.  Then I saw it... google-libphonenumber.  Unpacked, it's 720 KB, about 1/3 the size of the entire app!  That's a lot of javascript, for something so specific.  

It had to go.

So I thought, what can I do?  Class-validator uses it, and I want to use class-validator, it would be pretty disruptive to axe it.  I'm also not using it directly, so I'd have to fork a few packages, packages I really don't want to maintain.  Yarn resolutions are here to save me.  I can force google-libphonenumber to be any package I want!  Someone suggested a package called validator, it looked good enough.  I got to hacking.

Luckily, it wasn't heavily used.  I found this in class-validator:

private libPhoneNumber = {
 phoneUtil:require("googlelibphonenumber").PhoneNumberUtil.getInstance(),
};

Okay, so there's an object that has a function, easy enough, so far.  It was only referenced twice:

const phoneNum = this.libPhoneNumber.phoneUtil.parseAndKeepRawInput(value, region); 

return this.libPhoneNumber.phoneUtil.isValidNumber(phoneNum);

So getInstance returns two functions, I need to care about.  I made a new repo, a package.json file, and in my index.json went this:

import isMobilePhone from 'validator/lib/isMobilePhone'

const PhoneNumberUtil = {
    getInstance() {
        var locale = ''
        return {
            parseAndKeepRawInput(val, locale) {
                locale = locale
                return val
            },
            isValidNumber(phoneNum) {
                return isMobilePhone(phoneNum, locale)
            }
        }
    }
}

export { PhoneNumberUtil }
That's it.  Really.  

Well, okay, I had to swap it out, too.  I added google-libphonenumber-shim to my dependencies and one more magic line:

"resolutions": {
    "google-libphonenumber" : "git+https://github.com/Doelfke/google-libphonenumber-shim.git"
  }

Now every time google-libphonenumber is referenced, my new version is returned, instead. I will probably have to enhance it a bit, but the code, including the dependencies is only a couple hundred lines now.  Beautiful.

But did it work?  Oh, it did.  After minification and compression, we saved about 350 KB; not bad at all!  I can't say I recommend doing this every day, but it's a nice alternative to forking a package, or trying to get a PR in and accepted on time, on a package you do not own.  So I'm not saying to go start hacking at Lodash or something.  It is pretty harmless though; I should be able to easily take out the hack, if I ever need to.  

As a final thought, package size is important.  I think we often think a file that ends up with a small gzipped size means everything is a-ok.  But your browser has to decompress it, and that 350 KB of JavaScript again becomes 720 KB.  It's faster to download the smaller version, but it still has 720 KB of JavaScript to parse, compile, optimize, and hold in memory.  Happy hacking.