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

Mark Brown
Summit Technology Group
5 min readMay 8, 2023

At STG, we recently optimized one of our applications: a 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 bandwidth savings by way of declarative bundle splitting intersecting intentional browser caching. Stay tuned, there’s a surprise…

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 unmistakable fact that our application is large and complex. New features are being introduced and refinements are being made constantly which is awesome but also introduces a new class of problems. The application is on a frequent deployment cycle with scheduled releases with an occasional hotfix release. There are some pretty big features that offer a ton of utility but often times the refinements we make might only affect just a single component. 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…

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 958KB 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 647KB. 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.

Injecting Webpack Output into Django Templates

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 %}
<!DOCTYPE html>
<html>
<head>
<title>Summit Technology Group</title>
</head>
<body>
{% render_bundle 'app' %}
</body>
</html>

Refining Bundles by Splitting the Vendor Bundle

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 production webpack config adds a content hash to this name so if we upgrade one of these core dependencies, we’ll automatically get the desired cache-busting behavior)
  • assigned a priority (which module to split first.. -1 comes before -2, etc.)
  • reuseExistingChunk (so that later defined modules referencing pieces already packed will use the already packed chunk)
  • set enforce true so that webpack 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,
},
},
},
},
};

Our result here was a 180KB savings application-wide. Not too shabby.

While this 180KB savings looks really great, it 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.

Improving the way we import components

The process of dynamically importing/lazy loading, is super simple. You can do it to both page-level components that a route resolves to and components 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 candidate for this.

main.js

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

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

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!

--

--