09. Modules, DOM and Performance

Modules and the Development/Deploy Pipeline

SPAs consist of many JS and HTML templates which isn’t ideal in terms of resources and latency:

Bundling is the process of taking the source code and optimizing it into a format that is better for the browser to consume.

In its simplest form, bundling simply concatenates all JS files into one big file (and the same thing for CSS) - tools such as Gulp follow this pattern.

Minification

The build process will also often minify (or Uglify) the JS by removing functions, whitespace, reducing function/variable names (except global function names cannot be renamed) etc.

To aid in debugging, sourcemaps can be made so that the dev tools can present the code as it originally was.

Webpack

Current best practice. To configure webpack, simply point it to the source directory and the app starting point - it will then recursively follow imports, ensuring that only deadcode is removed.

The output is something like main.${hash}.js.

Webpack supports TypeScript and other transpiled languages.

Lazy Loading

Breaks the SPA into logical chunks so that the entire JS code does not need to be downloaded at the start.

Framework and bundler need to agree on how to split the import graph - usually with bundler plugins.

To do it natively in Webpack:

// Instead of 
import Component from "/Component";

// Use
const Component = () => import("./Component");

Compression

To reduce network transfer (but not the cost of parsing the JS), use a compression algorithm such as gzip. There is a small CPU cost to decompress, but this is small on modern machines.

In the HTTP request, the browser adds a header indicating the encodings it supports e.g. accept-encoding: gzip, deflate, br. Then, the server can optionally encode it in one of the given options, indicating this using content-encoding: gzip.

Caching

Can add HTTP headers to allow the browser to cache content.

expires:

cache-control:

ETag caching

On first request, server responds with ETag: ${some_long_number} and cache-control: no-cache.

On subsequent requests, browser adds If-None-Match: ${etag}; if the ETag matches the current version, the server returns a 304 not modified.

This requires a round-trip and the server to be available, but allows for changes to instantly propagate to all clients.

Caching in a SPA

index.html: use a cache that suits the release cadence. Probably an ETag.

All other file names contain their hash, so you can set cache-control: max-age=315360000 (one year).

Modules

Originally, JS functions existed in the global namespace and had no way of partitioning code. Hence, there was no way to protect module-internal symbols.

Modules solve the problem but… it was solved multiple times:

ES2015

ES2015 is:

// lib.js
export default export0;
export const export1;
export const export2;

// main.js
// Import specific components
import theDefaultExport, { export1, export2 as alias } from "lib";

// To import anything
import * as lib from "lib";

// Run global code but don't import anything
import "lib";

CommonJS

CommonJS was adopted early on by NodeJS:

// lib.js
exports.export1 = "bla";

// main.js
const lib = require("./lib.js");

The DOM

JS can dynamically change any element in the DOM as well as their properties, including their CSS styles.

The DOM can become excessively large in some applications, with performance being hurt when a large number of nodes need to be modified.

Virtual DOM

The virtual DOM, popularized by React, is an abstraction of the DOM that exists in the JS. The virtual DOM is first modified, and the real DOM is updated only if required. Because repainting is a very expensive operation so by batching changes, it can lead to increased performance.

Hence, when using libraries such as React and Vue, you do not work directly with the DOM.

In order to update the virtual DOM, it needs to know when the state has changed. To do this it has two main methods:

When the state is updated, it needs to compute the diff so that only the required DOM nodes are updated.

DOM-based XSS Injection

XSS: when a malicious script is run on a trusted side, getting full access to the DOM and cookies.

Hence, when manipulating the DOM, care must be taken to ensure that untrusted data is only ever treated as displayable text, not executed as code. This can be done by avoiding dangerous functions such as eval, innerHTML. Sanitizing the input data is difficult as some payloads can take advantage of browsers helpfully ‘fixing’ malformed HTML that gets through the sanitizer.

Performance

Response Times: 3 Important Limits

100 milliseconds: the limit for having the user feel the system is reacting instantaneously

1 seconds: the limit for the user’s flow of thought staying uninterrupted

10 seconds: the limit for keeping the user’s attention focused on the program

Yahoo Performance Rules

Lazy Loading with Intersection Observer

Intersection Observer API: register an element and a callback is executed when the element enters/exits another element, the viewport, or when the amount of intersection changes enough.

const observer = new IntersectionObserver(callback, {
  root: document.querySelector("#elementToObserve"),
  rootMargin: "0px 0px 0px 0px", // margin surrounding the root element to grow/shrink its bounding box
  threshold: 0.5 // 0.0 means if a single pixel is visible, the target is visible. 1.0 means the entire element must be visible
})

Progressive Web Apps

Web apps that appear to be ‘installed’ like native applications.

Service workers allow notifications and background sync (offline cache). Service workers: