Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript optimizations #8

Closed
addyosmani opened this issue Apr 18, 2018 · 3 comments
Closed

JavaScript optimizations #8

addyosmani opened this issue Apr 18, 2018 · 3 comments
Assignees

Comments

@addyosmani
Copy link
Collaborator

Roughly:

  • Capture screenshots of JS size/processing time analysis before we make any improvements
  • Start dynamically importing more of our routes as they are needed (code-splitting)
  • Look at whether we need better vendor splitting of library code
  • Are we using any large libraries? May be worth highlighting how easy this can be a problem
  • Attempt to use webpack bundle analyzer to find wins (or something like bundle buddy)
  • Show the before/after impact of these changes.
@addyosmani addyosmani self-assigned this Apr 18, 2018
@addyosmani
Copy link
Collaborator Author

addyosmani commented Apr 18, 2018

Optimization: Reduce bloated dependencies to keep JS payloads smaller.

Step 1: Measure

We start out by testing npm run dev. We know Vue doesn't apply all production optimizations at this point. We're looking at a 2.3MB payload overall:

image

Of this, 1.7MB of the payload is JavaScript:

image

Let's try running the production build to see how this changes.

Thanks to Vue's usage of the Webpack performance budget feature, we're able to highlight [big] files:

image

Lots of large PNGs/images but also a very large 2.16MB JavaScript vendor file. The rest of our application JavaScript is pretty small.

We can use Lighthouse to analyze how we're doing on performance:

image

We have large network payloads:

image

A lot of the initial JS bootup costs are low because we aren't doing a whole lot when the app first starts. Most of the UI that gets executed is on the next route:

image

And we can see that Parse/Compile of JS takes a few hundred milliseconds:

image

But enormous payloads are still the biggest problem.

Let's try analyzing this bundle in some more depth. Following https://code.luasoftware.com/tutorials/vuejs/vuejs-webpack-analyze-bundle-size/, we know we can run npm run build --report to run Webpack Bundle Analyzer.

It gives us an output that looks like this:

image

with the drawer

image

That unicode (SO.js) module seems troublesome! The parsed cost is 1.64MB of code, which we know will take a few seconds to parse and gzipped that code is still ~93KB.

image

Where do we use this module?

  • Having a hard time finding it. I looked in the source. No unicode or so.js
  • Looked in package.json and lockfile. Seems it might be tied to a babel transform for regex/unicode
  • Digging through npm, I see that this unicode module is relied upon by https://www.npmjs.com/package/slug (which we use for slugifying URLs). Let's try to replace it with something much slimmer.

We go to our editor, and because we're using the import-cost plugin in VSCode, we can see that yes, slug does indeed take up a bunch of extra bytes and has a heavy weight:

image

BundlePhobia is a nice tool for finding out the consumption weight of different npm modules. We use it to test the slug module as follows:

image

and can then try out alternatives. I find one called Limax which is 42KB gzipped: https://bundlephobia.com/result?p=limax@1.6.0 and another called https://bundlephobia.com/result?p=slugify@1.2.9 which is 4.2KB gzipped. Let's try that one.

Step 2: Optimize

Looks pretty okay in editor and bundlephobia. Let's use the slugify module.

image

image

When we make this change to our app, we can see that Vue's webpack build report changes:

image

We have now saved 1.72MB on the size of the bundle and it's down to 444KB.

This audit now passes the enormous payloads check in Lighthouse:

image

and here's some bumps in performance metrics:

image

Success 🎉

@addyosmani
Copy link
Collaborator Author

addyosmani commented Apr 18, 2018

Optimization: Remove unused/unnecessary dependencies to reduce JS payload size

Originally, we made changes to our app because we were experimenting with using Material Design - cards, toolbars, nav. Later, once we decided to go more custom, we kept these imports in and they were bloating our bundle.

We can also see that the Vue MDC adapter is taking up a chunk of our bundle:

image

Looking back after the last optimization, we were at 444KB for the size of the bundle.

image

Lighthouse audits for main thread consumption and JS bootup time:

image

Dropping the MDC Web JavaScript only, we're able to trim the bundles down much more heavily.

image

We can still use the CSS styles (which we import) as needed.

We shaved off 327KB off our bundle.

Everything seems to work as expected:

image

Lighthouse wise, this takes us from this:

to this:

image

Our network payloads and JS bootup time are down

image

image

@addyosmani
Copy link
Collaborator Author

addyosmani commented Apr 18, 2018

Optimization - code-splitting

Note: this may not be entirely practical for now, so documenting the change but not including in PR just yet.

Originally, we're looking at a 12-13KB app.js size:

This is before we even considered doing anything with code-splitting etc.

image

Let's look at the old code before we add code-splitting:

import DoodleHome from '@/components/DoodleHome'
import DoodleBrowse from '@/components/DoodleBrowse'
import DoodleFullscreen from '@/components/DoodleFullscreen'
import DoodleOffline from '@/components/DoodleOffline'

and after:

const DoodleHome = () => import('@/components/DoodleHome')
const DoodleBrowse = () => import('@/components/DoodleBrowse')
const DoodleFullscreen = () => import('@/components/DoodleFullscreen')
const DoodleOffline = () => import('@/components/DoodleOffline')

and if we want to name these chunks:

const DoodleHome = () => import(/* webpackChunkName: "doodle-home" */ '@/components/DoodleHome')
const DoodleBrowse = () => import(/* webpackChunkName: "doodle-browse" */'@/components/DoodleBrowse')
const DoodleFullscreen = () => import(/* webpackChunkName: "doodle-fullscreen" */'@/components/DoodleFullscreen')
const DoodleOffline = () => import(/* webpackChunkName: "doodle-offline" */'@/components/DoodleOffline')

Post-build, we're looking at some pretty small sized bundles now for each route:
image

With named chunks, it looks like this:

image

However...

I now notice a short lag before the doodles browser view loads. As the previous app chunks were small enough (12KB) we might want to suggest we tried code-splitting but it wasn't needed in the end, or, try to inflate the size of a route artificially so that the benefit is more pronounced.

addyosmani added a commit that referenced this issue Apr 20, 2018
…nto development

* 'development' of https://github.com/google/io-demo-app:
  JS perf: Drop MVC JS from the bundle (#8)
  JS perf: switch from slug to slugify module
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant