This can quickly turn into a poor and expensive user experience, yet it’s all but free for the developer. Storage and server bandwidth are incredibly cheap, only becoming expensive when dealing with gigabytes of data delivered at a vast scale.
But what happens when you’re suddenly limited by your storage?
Limited not to hundreds of megabytes, but only a few. One of my recent projects was to develop a fast WiFi setup page for a device built with an embedded system housing a total of 4MB of storage space.
The boot loader and system libraries are given 1MB of space, leaving just 3MB. However, that memory is then divided into thirds:
- One for the currently running firmware
- One for the incoming firmware update, and
- One for the factory default firmware.
That’s just 1MB for the entire system, including the webpage. That page of search results from Google? It wouldn’t even fit by itself.
Google search results page for “large webpages” loads 1.34MB of data when uncompressed. We’ll come back to that number momentarily.
The purpose of the webpage is to assist the user in finding nearby WiFi connections and connecting the device to them. Though the device is internet-connected, it is by no means a “high tech” type of device; the consumers are not necessarily first-adopter technophiles.
The webpage needs to work on as many browsers as possible and stay on-brand to the product to assure people that they are in the right place.
Let me walk you through some of the methods I used to make this page as lightweight as possible with a good user experience, while still easy to work on as a developer.
The first easy win was to not use any 3rd-party libraries. Libraries and frameworks are useful, but always contain code that isn’t used for a particular project. With our small amount of storage space, this project couldn’t afford any unused bytes.
To help with CSS browser compatibility, I used SCSS and Autoprefixer (while I didn’t have a good knowledge at the time of Typescript, I did of SCSS and was confident I wouldn’t be adding unnecessary bloat). Where it didn’t matter, I lived with design inconsistencies.
As wiser people before me have said, a webpage does not need to look the same in every browser. This meant not bringing in Normalize.css or a similar library to help smooth the differences between browsers.
As an example of minor inconsistencies, I used CSS Grids to layout one of the components. The modern syntax isn’t supported in Internet Explorer but could have been made to work with Autoprefixer. Doing so turned out to generate so much extra code that I decided a slight difference in layout was acceptable and opted to use floats instead.
To keep both markup and CSS sleek, the majority of the selectors were written using the semantic markup of the HTML rather than sprinkling extra classes around.
The only image on the site is the logo. To begin, I worked with the designer to convert it to SVG format. SVGs have a lot of optimisation opportunities. I ran it through SVGOMG, selecting the lowest precision while still maintaining the shapes. From there, the file was further optimised by putting the styles in-line rather than using classes. This may seem counter-intuitive as it creates more code, but it allows for improved compression, which is covered later.
I used the task runner Grunt to compile and minify my sources and then put all the pieces together to build the webpage.
First, the SCSS gets compiled with a compressed style and no sourcemap. This removes comments and whitespace. From there cross-browser styles are applied with Autoprefixer.
The scripts are processed using Uglify which does a lot to minify the scripts. Mangling (to shorten variable and function names to a single character) was used with the
toplevel option set to true as there was no namespacing to worry about.
The last couple of steps were to insert the compiled CSS and minified JS directly into the HTML file. Because the embedded system isn’t running a true web server, it was much simpler to create a single GET endpoint for the index.html file. This also reduced firmware code.
With those files injected, the entire HTML file was minified: comments removed, whitespace collapsed safely, plus attributes and class names sorted. This last piece, the sorting, was to help with compression, the final task.
By far the biggest savings came from GZipping the file. Since the embedded system isn’t going to compress the file on the fly, GZipping was done during the build process.
Simply put, GZipping works by replacing repeated characters with references. This is why putting HTML attributes into the same order is important as it creates larger and more repeatable patterns, resulting in higher compression ratios.
I took a dozen calls similar to
var $form = document.querySelector("form"); and converted them to this:
var q = document.querySelector.bind(document); var $form = q("form"); ...
While this meant the minifier could get rid of a lot of extra bytes, it wasn’t as efficient for GZip. Discovering these non-intuitive optimisation methods was a rewarding part of the project.
Recall that the “partition” for the firmware is only 1MB. The firmware size is a bit more than 900kB all told, most of that necessary libraries, leaving around 80kB of breathing room.
The webpage is 4.5kB compressed. While I may not have needed to worry about every little byte, adding in a library or being careless with an image could easily have put it above that 80kB threshold. And with over-the-air updates possible, those 80kB may be needed for future improvements!
It was a good little project and fun to see what could be done with such a small amount of available space. Interactive webpages do not need all the frameworks or all the code to be successful.
If you need to build custom Web, Mobile, or Embedded software solutions, contact Intranel to discuss how we can help turbo-boost your project.