Skip to content

Instantly share code, notes, and snippets.

@markwbrown
Created April 18, 2023 03:05
Show Gist options
  • Save markwbrown/5dfbf87835455615fd3ad998f335be9a to your computer and use it in GitHub Desktop.
Save markwbrown/5dfbf87835455615fd3ad998f335be9a to your computer and use it in GitHub Desktop.
STG webpack bundle optimization

How we optimized our Hybrid Django/Vue Project at Summit Technology Group and cut our bundle size by 25%.

At STG, we recently optimized the ulp dev/disaster branch, our hybrid Django/Vue project. In this post, we'll share our experience using webpack-bundle-analyzer, django-webpack-loader, and webpack-bundle-tracker to assess and refine our build process, ultimately leading to significant performance improvements, particularly with respect to shared assets between builds.

But Mark, you say, why go through all this trouble?

Our previous process had django as arbiter of all files in the project. We had already leveraged whitenoise on top of django's staticfiles storage class, so there were already some good practices in place with respect to appending content hashes to files, delivering files in compressed format, and handling headers better than Zamorano. Still, we were hitting up against the unmistakealbe fact that our application is, by nature, not a small thing. There are some pretty big features that offer a ton of utility but often times the refinements we make affect just a single component. Did I mention that this application is also on a frequent deployment cycle? New features are being introduced and refinements are being made constantly which is awesome but also introduces a new class of problems. If we went with webpack defaults, we'd be invalidating the entire browser cache every couple weeks and shipping extra code where it might not ever be needed for a particular user. That's a lot of extra bandwidth which costs money, can be a scarce resource, and comes at the expense of performance. If the application is not performant, the user experience suffers. So let's not do that... Just a bit of back-of-the-napkin math... for a 37.8k sba-disaster-map.js lazy-loaded import, it would take ~250million downloads to reach AWS the top of the AWS cloudfront sub 10tb transfer out pricing. Priced per gigabyte, that's $870.40 USD for the first 10tb. While I think it would take awhile to get to 250 million downloads of that file, lazy loading imports has the major upside of saving a ton of money in transfer-out costs on cloudfront. The benefits stack up rather quickly when we scale this application-wide and there are ~40,000 daily users.

Initial Assessment with Webpack-Bundle-Analyzer

Before starting in on the optimizations, we used webpack-bundle-analyzer to generate an interactive treemap visualization of our bundles. This allowed us to set up a baseline and test how different bundling strategies affected our bundle size. A 958k vendor bundle meant that any update to a dependency -- even removing a dependency would make useless nearly a megabyte of data that had already been downloaded. Even more problematic was our app bundle weighing in at 646k. Anytime we made a change to a component, it was like we were taking eleven eggs and smashing them on the floor, running to the grocery store and picking up another carton of eggs because that one egg from the first dozen had a bit of shell in it. Initial Bundle Analysis

Injecting Webpack Output into Django Templates

Next, we used django-webpack-loader and webpack-bundle-tracker to inject the webpack output into our Django templates. This simplified the integration between Django and Vue, ensuring that our templates always had access to the latest compiled assets.

###settings.py

WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': 'bundles/', 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'), } }

###Django templates

`{% load render_bundle from webpack_loader %}

<title>Summit Technology Group</title> {% render_bundle 'app' %} `

##Refining Bundles by Splitting Vendor and Utilizing Dynamic Imports

We initially focused on splitting out constituent pieces of the vendor bundle. We wanted the ability to add and remove dependencies to the application without smashing all the eggs we had already collected. For this, we used the splitChunksPlugin. You'll notice in our configuration we named the chunks (our webpack config adds a content hash to this name so if we upgrade one of these core dependencies, we'll automaticlaly get the desired cache-busting behavior), assigned a priority (which module to split first), reuseExistingChunk (so that later defined modules referencing pieces already packed will use the already packed chunk), and set enforce true so that wepack puts these rules ahead of anything that might be in conflict such as maxSize, maxAsyncRequests, or maxInitialRequests.

###webpack.config.js


module.exports = {
    // ...
    plugins: [
        new BundleTracker({filename: './webpack-stats.json'}),
        // ...
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
            		vue: {
			          test: /[\\/]node_modules[\\/](vue|@vue\/)[\\/]/,
			          priority: -1,
			          name: 'vue',
			          chunks: 'initial',
			          enforce: true,
			          reuseExistingChunk: true,
			        },
			        vuetify: {
			          test: /[\\/]node_modules[\\/](vuetify)[\\/]/,
			          priority: -2,
			          name: 'vuetify',
			          chunks: 'initial',
			          enforce: true,
			          reuseExistingChunk: true,
			        },
			        tiptapvuetify: {
			          test: /[\\/]node_modules[\\/](tiptap-vuetify)[\\/]/,
			          priority: -3,
			          name: 'tiptap-vuetify',
			          chunks: 'initial',
			          enforce: true,
			          reuseExistingChunk: true,
			        },
			        vueExtras: {
			          test: /[\\/]node_modules[\\/](|vuex|vue-router)[\\/]/,
			          priority: -4,
			          name: 'vueExtras',
			          chunks: 'all',
			          enforce: true,
			          reuseExistingChunk: true,
			        },
			        uiComponentLib: {
			          test: /[\\/]node_modules[\\/](@thesummitgrp\/los-app-ui-component-lib)[\\/]/,
			          priority: -5,
			          name: 'ui-component-lib',
			          chunks: 'all',
			          reuseExistingChunk: true,
			        },
            },
        },
    },
};

Splitting Vendors Bundle Analysis

The casual observer will point out that the 400k savings * 40,000 daily users mentioned before only comes out to 16 gigabytes of data transfer. This fails to capture the full effect of splitting the bundle. There are parts of our application that don't ever use a particular dependency. By splitting the vendor bundle to its constituent parts, only users who need that chunk will download it. This pattern gets really interesting in its implications when we apply what we did to the vendor bundle to the app bundle. Huge segments of this application's users will never touch all pieces of the application.

You have my attention, now make this puppy a rocket ship...

The process of dynamically importing (really, it's just lazy loading), is super simple. You can do it to both page-level components that a route resolves to and pieces on that page. For example, a really long form that has multiple execution paths that have wildly different behaviors based upon the chosen path is a great canidate for this.

###main.js

const ComponentA = () => import(/* webpackChunkName: "componentA" */ './components/ComponentA.vue');

const ComponentB = () => import(/* webpackChunkName: "componentB" */ './components/ComponentB.vue');

When we went though and assessed the impact of lazy loading our entire application, we found ourselves with an application that was half a megabyte lighter in total, with cache-busting casualties limited to only components that had changed since our last deployment. Pure awesome.

Pure Asesome Bundle Analysis

Interested in working with AWS, Kubernetes, full stack engineering, or Data Engineering in a fast-paced environment? Go check out our open positions at thesummitgrp.com!

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