Embedding SVG files in a JS bundle using Webpack

Cause

Recent issues with the internet in Tajikistan made me think about the loading speed of my online dictionary Vazhaju (Вожаҷӯ / واژه‌جو). As soon as the Tajik government announced that they would raise mobile internet prices I rushed to check the loading speed of the site. The speed was high, the site had been tailored for mobile devices, was served using GZIP compression and practically didn’t have anything unnecessary.

But then I noticed an interesting detail, the actual size of the content didn’t exceed 7kb, whereas every page in it’s compressed form took at least 12kb, and the uncompressed form took around 24kb. Not such a tiny size for so little content, isn’t it. This size inflation was caused by the SVG images that were added in their entirety to the main template on each page serving.

A little bit on the architecture

The front end is structured as a set of independent micro applications, aka micro front ends. There’s one for public users, one for admins, one for editors, etc, therefore,

  1. Not all of the applications needed the entire SVG package, each used only a few images.
  2. I could take advantage of the browser cache, load the images only once for each of my micro applications and simply serve them from there.

The solution was quite obvious, generate individual SVG sprite for each micro front end, embed it in it’s JS bundle, and add the sprite to the DOM on page load.

Search for an existing solution

I embarked on a quite lengthy search for Webpack loaders and plugins that could solve my problem. To my surprise existing loaders wouldn’t allow me to create an individual file per each micro application. They could only gather all of the SVG images used in all of the bundles and combine them into one gigantic SVG sprite oneGiganticSprite.svg, that all of the micro applications had to use. It was exactly what I was trying to avoid.

Even if I managed to build individual sprites, I still had to somehow load it. There were recommendations to asynchronously load the sprite file on page load and add it to the DOM. This approach has three huge downsides:

  1. Need for an HTTP library, which means increase of the size of my bundle
  2. Extra HTTP call
  3. Implementation of custom caching

All this could be avoided by simply including the sprites in the JS bundle and all of this would be taken care of by the built-in browser functionality.

Finally I gave up looking for third party solutions and decided to write my own. I needed write a custom Webpack SVG loader, to gather the necessary SVG images and add them to individual JS bundles.

To my luck it turned out to be very simple.

Implementation

Before you continue, I recommend you to look at the sample project containing all of the provided here source code.

Custom Webpack loader

svgSpriteLoader.js - The loader almost entirely copies the Rawloader except for the last line

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// svgSpriteLoader.js
const { getOptions } = require('loader-utils');
const validateOptions = require('schema-utils');
const path = require('path');
const schema = {};

module.exports = function svgSpriteLoader(source) {
const options = getOptions(this) || {};
validateOptions(schema, options, 'Svg Sprite Loader');

const loaderContext = this;
const { resourcePath } = loaderContext;

let json = JSON.stringify(source)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');

return `export default { fileName: '${path.basename(resourcePath)}', svg: ${json} }`;
};

Read More