e

Responsible JavaScript: Part II

You and the rest of the dev team lobbied enthusiastically for a total re-architecture of the company’s aging website. Your pleas were heard by management—even up to the C-suite—who gave the green light. Elated, you and the team started working with the design, copy, and IA teams. Before long, you were banging out new code.

It started out innocently enough with an npm install here and an npm install there. Before you knew it, though, you were installing production dependencies like an undergrad doing keg stands without a care for the morning after.

Then you launched.

Unlike the aftermath of most copious boozings, the agony didn’t start the morning after. Oh, no. It came months later in the ghastly form of low-grade nausea and headache of product owners and middle management wondering why conversions and revenue were both down since the launch. It then hit a fever pitch when the CTO came back from a weekend at the cabin and wondered why the site loaded so slowly on their phone—if it indeed ever loaded at all.

Everyone was happy. Now no one is happy. Welcome to your first JavaScript hangover.

It’s not your fault

When you’re grappling with a vicious hangover, “I told you so” would be a well-deserved, if fight-provoking, rebuke—assuming you could even fight in so sorry a state.

When it comes to JavaScript hangovers, there’s plenty of blame to dole out. Pointing fingers is a waste of time, though. The landscape of the web today demands that we iterate faster than our competitors. This kind of pressure means we’re likely to take advantage of any means available to be as productive as possible. That means we’re more likely—but not necessarily doomed—to build apps with more overhead, and possibly use patterns that can hurt performance and accessibility.

Web development isn't easy. It’s a long slog we rarely get right on the first try. The best part of working on the web, however, is that we don’t have to get it perfect at the start. We can make improvements after the fact, and that’s just what the second installment of this series is here for. Perfection is a long ways off. For now, let’s take the edge off of that JavaScript hangover by improving your site’s, er, scriptuation in the short term.

Round up the usual suspects

It might seem rote, but it’s worth going through the list of basic optimizations. It’s not uncommon for large development teams—particularly those that work across many repositories or don’t use optimized boilerplate—to overlook them.

Shake those trees

First, make sure your toolchain is configured to perform tree shaking. If tree shaking is new to you, I wrote a guide on it last year you can consult. The short of it is that tree shaking is a process in which unused exports in your codebase don’t get packaged up in your production bundles.

Tree shaking is available out of the box with modern bundlers such as webpack, Rollup, or Parcel. Grunt or gulp—which are not bundlers, but rather task runners—won’t do this for you. A task runner doesn’t build a dependency graph like a bundler does. Rather, they perform discrete tasks on the files you feed to them with any number of plugins. Task runners can be extended with plugins to use bundlers to process JavaScript. If extending task runners in this way is problematic for you, you’ll likely need to manually audit and remove unused code.

For tree shaking to be effective, the following must be true:

  1. Your app logic and the packages you install in your project must be authored as ES6 modules. Tree shaking CommonJS modules isn’t practically possible.
  2. Your bundler must not transform ES6 modules into another module format at build time. If this happens in a toolchain that uses Babel, @babel/preset-env configuration must specify modules: false to prevent ES6 code from being converted to CommonJS.

On the off chance tree shaking isn’t occurring during your build, getting it to work may help. Of course, its effectiveness varies on a case-by-case basis. It also depends on whether the modules you import introduce side effects, which may influence a bundler’s ability to shake unused exports.

Split that code

Chances are good that you’re employing some form of code splitting, but it’s worth re-evaluating how you’re doing it. No matter how you’re splitting code, there are two questions that are always worth asking yourself:

  1. Are you deduplicating common code between entry points?
  2. Are you lazy loading all the functionality you reasonably can with dynamic import()?

These are important because reducing redundant code is essential to performance. Lazy loading functionality also improves performance by lowering the initial JavaScript footprint on a given page. On the redundancy front, using an analysis tool such as Bundle Buddy can help you find out if you have a problem.

Bundle Buddy can examine your webpack compilation statistics and determine how much code is shared between your bundles.

Where lazy loading is concerned, it can be a bit difficult to know where to start looking for opportunities. When I look for opportunities in existing projects, I’ll search for user interaction points throughout the codebase, such as click and keyboard events, and similar candidates. Any code that requires a user interaction to run is a potentially good candidate for dynamic import().

Of course, loading scripts on demand brings the possibility that interactivity could be noticeably delayed, as the script necessary for the interaction must be downloaded first. If data usage is not a concern, consider using the rel=prefetch resource hint to load such scripts at a low priority that won’t contend for bandwidth against critical resources. Support for rel=prefetch is good, but nothing will break if it’s unsupported, as such browsers will ignore markup they doesn’t understand.

Externalize third-party hosted code

Ideally, you should self-host as many of your site’s dependencies as possible. If for some reason you must load dependencies from a third party, mark them as externals in your bundler’s configuration. Failing to do so could mean your website’s visitors will download both locally hosted code and the same code from a third party.

Let’s look at a hypothetical situation where this could hurt you: say that your site loads Lodash from a public CDN. You've also installed Lodash in your project for local development. However, if you fail to mark Lodash as external, your production code will end up loading a third party copy of it in addition to the bundled, locally hosted copy.

This may seem like common knowledge if you know your way around bundlers, but I’ve seen it get overlooked. It’s worth your time to check twice.

If you aren’t convinced to self-host your third-party dependencies, then consider adding dns-prefetch, preconnect, or possibly even preload hints for them. Doing so can lower your site’s Time to Interactive and—if JavaScript is critical to rendering content—your site’s Speed Index.

Smaller alternatives for less overhead

Userland JavaScript is like an obscenely massive candy store, and we as developers are awed by the sheer amount of open source offerings. Frameworks and libraries allow us to extend our applications to quickly do all sorts of stuff that would otherwise take loads of time and effort.

While I personally prefer to aggressively minimize the use of client-side frameworks and libraries in my projects, their value is compelling. Yet, we do have a responsibility to be a bit hawkish when it comes to what we install. When we’ve already built and shipped something that depends on a slew of installed code to run, we’ve accepted a baseline cost that only the maintainers of that code can practically address. Right?

Maybe, but then again, maybe not. It depends on the dependencies used. For instance, React is extremely popular, but Preact is an ultra-small alternative that largely shares the same API and retains compatibility with many React add-ons. Luxon and date-fns are much more compact alternatives to moment.js, which is not exactly tiny.

Libraries such as Lodash offer many useful methods. Yet, some of them are easily replaceable with native ES6. Lodash’s compact method, for example, is replaceable with the filter array method. Many more can be replaced without much effort, and without the need for pulling in a large utility library.

Whatever your preferred tools are, the idea is the same: do some research to see if there are smaller alternatives, or if native language features can do the trick. You may be surprised at how little effort it may take you to seriously reduce your app’s overhead.

Differentially serve your scripts

There’s a good chance you’re using Babel in your toolchain to transform your ES6 source into code that can run on older browsers. Does this mean we’re doomed to serve giant bundles even to browsers that don’t need them, until the older browsers disappear altogether? Of course not! Differential serving helps us get around this by generating two different builds of your ES6 source:

  • Bundle one, which contains all the transforms and polyfills required for your site to work on older browsers. You’re probably already serving this bundle right now.
  • Bundle two, which contains little to none of the transforms and polyfills because it targets modern browsers. This is the bundle you’re probably not serving—at least not yet.

Achieving this is a bit involved. I’ve written a guide on one way you can do it, so there’s no need for a deep dive here. The long and short of it is that you can modify your build configuration to generate an additional but smaller version of your site’s JavaScript code, and serve it only to modern browsers. The best part is that these are savings you can achieve without sacrificing any features or functionality you already offer. Depending on your application code, the savings could be quite significant.

A webpack-bundle-analyzer analysis of a project's legacy bundle (left) versus one for a modern bundle (right). View full-sized image.

The simplest pattern for serving these bundles to their respective platforms is brief. It also works a treat in modern browsers:

<!-- Modern browsers load this file: -->
<script type="module" src="/js/app.mjs"></script>
<!-- Legacy browsers load this file: -->
<script defer nomodule src="/js/app.js"></script>

Unfortunately, there’s a caveat with this pattern: legacy browsers like IE 11—and even relatively modern ones such as Edge versions 15 through 18—will download both bundles. If this is an acceptable trade-off for you, then worry no further.

On the other hand, you'll need a workaround if you’re concerned about the performance implications of older browsers downloading both sets of bundles. Here’s one potential solution that uses script injection (instead of the script tags above) to avoid double downloads on affected browsers:

var scriptEl = document.createElement("script");

if ("noModule" in scriptEl) {
  // Set up modern script
  scriptEl.src = "/js/app.mjs";
  scriptEl.type = "module";
} else {
  // Set up legacy script
  scriptEl.src = "/js/app.js";
  scriptEl.defer = true; // type="module" defers by default, so set it here.
}

// Inject!
document.body.appendChild(scriptEl);

This script infers that if a browser supports the nomodule attribute in the script element, it understands type="module". This ensures that legacy browsers only get legacy scripts and modern browsers only get modern ones. Be warned, though, that dynamically injected scripts load asynchronously by default, so set the async attribute to false if dependency order is crucial.

Transpile less

I’m not here to trash Babel. It’s indispensable, but lordy, it adds a lot of extra stuff without your ever knowing. It pays to peek under the hood to see what it’s up to. Some minor changes in your coding habits can have a positive impact on what Babel spits out.

https://twitter.com/_developit/status/1110229993999777793

To wit: default parameters are a very handy ES6 feature you probably already use:

function logger(message, level = "log") {
  console[level](message);
}

The thing to pay attention to here is the level parameter, which has a default of “log.” This means if we want to invoke console.log with this wrapper function, we don’t need to specify level. Great, right? Except when Babel transforms this function, the output looks like this:

function logger(message) {
  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";

  console[level](message);
}

This is an example of how, despite our best intentions, developer conveniences can backfire. What was a handful of bytes in our source has now been transformed into much larger in our production code. Uglification can’t do much about it either, as arguments can’t be reduced. Oh, and if you think rest parameters might be a worthy antidote, Babel’s transforms for them are even bulkier:

// Source
function logger(...args) {
  const [level, message] = args;

  console[level](message);
}

// Babel output
function logger() {
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
    args[_key] = arguments[_key];
  }

  const level = args[0],
        message = args[1];
  console[level](message);
}

Worse yet, Babel transforms this code even for projects with a @babel/preset-env configuration targeting modern browsers, meaning the modern bundles in your differentially served JavaScript will be affected too! You could use loose transforms to soften the blow—and that’s a fine idea, as they’re often quite a bit smaller than their more spec-compliant counterparts—but enabling loose transforms can cause issues if you remove Babel from your build pipeline later on.

Regardless of whether you decide to enable loose transforms, here’s one way to cut the cruft of transpiled default parameters:

// Babel won't touch this
function logger(message, level) {
  console[level || "log"](message);
}

Of course, default parameters aren’t the only feature to be wary of. For example, spread syntax gets transformed, as do arrow functions and a whole host of other stuff.

If you don’t want to avoid these features altogether, you have a couple ways of reducing their impact:

  1. If you’re authoring a library, consider using @babel/runtime in concert with @babel/plugin-transform-runtime to deduplicate the helper functions Babel puts into your code.
  2. For polyfilled features in apps, you can include them selectively with @babel/polyfill via @babel/preset-env’s useBuiltIns: "usage" option.

This is solely my opinion, but I believe the best choice is to avoid transpilation altogether in bundles generated for modern browsers. That’s not always possible, especially if you use JSX, which must be transformed for all browsers, or if you’re using bleeding edge language features that aren’t widely supported. In the latter case, it might be worth asking if those features are really necessary to deliver a good user experience (they rarely are). If you arrive at the conclusion that Babel must be a part of your toolchain, then it’s worth peeking under the hood from time to time to catch suboptimal stuff Babel might be doing that you can improve on.

Improvement is not a race

As you massage your temples wondering when this horrid JavaScript hangover is going to lift, understand that it’s precisely when we rush to get something out there as fast as we possibly can that the user experience can suffer. As the web development community obsesses on iterating faster in the name of competition, it’s worth your time to slow down a little bit. You’ll find that by doing so, you may not be iterating as fast as your competitors, but your product will be faster than theirs.

As you take these suggestions and apply them to your codebase, know that progress doesn’t spontaneously happen overnight. Web development is a job. The truly impactful work is done when we’re thoughtful and dedicated to the craft for the long haul. Focus on steady improvements. Measure, test, repeat, and your site’s user experience will improve, and you’ll get faster bit by bit over time.

Special thanks to Jason Miller for tech editing this piece. Jason is the creator and one of the many maintainers of Preact, a vastly smaller alternative to React with the same API. If you use Preact, please consider supporting Preact through Open Collective.




e

Optimizing Scratch 3 Pen Blocks

Earlier this year, we shared our work on the launch of Scratch 3.0, a major version of the visual programming environment for children of all ages. The new version of Scratch marked a complete rewrite of the runtime in JavaScript leveraging open web APIs. In our previous post, we enumerated the many performance optimizations that […]



  • Graphics And Interactive Applications

e

Take the MDN Developer & Designer Needs Survey

Today, MDN announced their first-ever needs assessment survey for web developers and designers. The survey takes about 20 minutes and asks a variety of questions aimed at understanding the joys, frustrations, needs and wants of everyday web-makers. Mozilla have committed to making the results of the survey public later this year, and the survey itself […]




e

Lessons Learned from a Year of Testing the Web Platform

The web-platform-tests project is a massive suite of tests (over one million in total) which verify that software (mostly web browsers) correctly implement web technologies. It’s as important as it is ambitious: the health of the web depends on a plurality of interoperable implementations. Although Bocoup has been contributing to the web-platform-tests, or “WPT,” for […]




e

Contributing to the ARIA Authoring Practices Guide

We believe that inclusivity and accessibility are of utmost importance to the open web platform. One of the ways that we empower the full diversity of Internet users is by ensuring that those with permanent disabilities and temporary limitations, can browse websites with assistive technologies. Writing accessible code improves the experience of browsing a website […]




e

Experiments in Faster Scratch 3 Loading with Texture Atlases

One of the best parts of the Scratch community is the diversity of Scratch projects. Community members have used the Scratch programming language to create many different kinds of interactive applications, from full game engines to music sequencers. One genre is especially unique: Multiple Animator Projects, or MAPs. These Scratch projects compile animations from many […]




e

How to scribe at TPAC

Next week is TPAC in Fukuoka, Japan. This is an annual conference for all working groups in the W3C to meet face-to-face. Naturally, there is a desire to have a record of what is said in these meetings. This is done by people in the meeting taking turns to scribe. Even if you have attended […]




e

Introducing a JavaScript library for exploring Scratch projects: sb-util

Introduction We’re excited to introduce sb-util, a new JavaScript library that makes it easy to query Scratch projects via .sb3 files. This npm library allows developers (or even teachers and students) to parse and introspect Scratch projects for a range of purposes, from data visualization to custom tooling. Previously, working with Scratch project files required […]




e

The ECMAScribes

Did you know that in the process of standardizing JavaScript, TC39 publishes notes for each of their regular meetings? Every other month, over 50 “delegates” convene to discuss the future of the language, and the minutes they publish provide an incredible view into their discussions. Here’s what you can expect to find: a list of […]




e

Launching Test262 Results on MDN Web Docs

We are excited to announce support for report embedding on test262.report, along with a new MDN collaboration to bring up-to-date information about ECMAScript feature conformance to MDN Web Docs. Starting today, you can view test results from Test262 Report, updated daily and embedded directly on MDN pages for the newest ECMAScript features where interoperability and […]




e

Bocoup & Open Standards: A (Very Full) Year in Review

We’ve had a very productive year making web standards more open, predictable, and inclusive. As our standards liaison, my job is to spot opportunities for us to do that work externally, and to see where more support is needed. We still have a lot to do, but it’s nice to reflect on our accomplishments over […]




e

Class history and class practices in the periphery of capitalism / edited by Paul Zarembka

Dewey Library - HB501.C53 2019




e

Who wants what?: redistribution preferences in comparative perspective / David Rueda, Daniel Stegmueller

Dewey Library - HB523.R84 2019




e

Asian transformations: an inquiry into the development of nations / edited by Deepak Nayyar

Dewey Library - HC412.A85 2019




e

Why liberalism works: how true liberal values produce a freer, more equal, prosperous world for all / Deidre Nansen McCloskey

Dewey Library - HB72.M33 2019




e

A question of inequality: the politics of equal worth / Christopher Steed

Dewey Library - HB523.S78 2018




e

The Canadian environment in political context / Andrea Olive

Dewey Library - HC120.E5 O45 2019




e

The Russian job: the forgotten story of how America saved the Soviet Union from ruin / Douglas Smith

Dewey Library - HC340.F3 S55 2019




e

Who governs: legislatures, bureaucracies, or markets? / John H. Wood

Online Resource




e

Uncanny valley: a memoir / Anna Wiener

Dewey Library - HC107.C2 H5335 2020




e

Mongrel firebugs and men of property: capitalism and class conflict in American history / Steve Fraser

Dewey Library - HC110.C3 F73 2019




e

The Oxford handbook of consumption / edited by Frederick F. Wherry and Ian Woodward

Dewey Library - HC79.C6 O938 2019




e

Minsky / Daniel H. Neilson

Dewey Library - HB119.M48 N45 2019




e

The age of sustainability: just transitions in a complex world / Mark Swilling

Dewey Library - HC79.E5 S9144 2020




e

The ethical algorithm: the science of socially aware algorithm design / Michael Kearns and Aaron Roth

Dewey Library - HC79.I55 K43 2020




e

Behavioural macroeconomics: theory and policy / Paul de Grauwe and Yuemei Ji

Dewey Library - HB74.P8 G73 2019




e

Predatory value extraction: how the looting of the business corporation became the U.S. norm and how sustainable prosperity can be restored / William Lazonick and Jang-Sup Shin

Dewey Library - HB201.L39 2020




e

Laid waste!: the culture of exploitation in early America / John Lauritz Larson

Dewey Library - HC103.7.L36 2020




e

Globalization, the IMF, and international banks in Argentina: the model economic crisis / Christian Hernandez

Dewey Library - HC175.H47 2019




e

How rich countries got rich ... and why poor countries stay poor / Erik S. Reinert

Dewey Library - HC21.R425 2019




e

Arguing with zombies: economics, politics, and the fight for a better future / Paul Krugman

Dewey Library - HC106.84.K78 2020




e

Organicity: entropy or evolution / David Dobereiner

Dewey Library - HC79.E5 D625 2019




e

The dark side of nudges / by Maria Alejandra Caporale Madi

Dewey Library - HB74.P8 C365 2020




e

Failure or reform?: market-based policy instruments for sustainable agriculture and resource management / Stewart Lockie

Dewey Library - HC79.E5 L636 2019




e

Nine crises: fifty years of covering the British economy from devaluation to Brexit / William Keegan

Dewey Library - HC256.K44 2019




e

Italy's economic revolution: integration and economy in Republican Italy / Saskia T. Roselaar

Dewey Library - HC39.R67 2019




e

Demystifying economic markets and prices: understanding patterns and practices in everyday life / Gregory R. Woirol

Dewey Library - HB221.W64 2019




e

Whatever it takes: the battle for post-crisis Europe/ George Papaconstantinou

Dewey Library - HC240.P37 2020




e

Possessive individualism: a crisis of capitalism / Daniel W. Bromley

Dewey Library - HB501.B76 2019




e

Reinventing liberalism: the politics, philosophy and economics of early neoliberalism (1920-1947) / Ola Innset

Online Resource




e

Social democratic capitalism / Lane Kenworthy

Dewey Library - HB501.K46 2020




e

US infrastructure: challenges and directions for the 21st century / edited by Aman Khan and Klaus Becker

Dewey Library - HC110.C3 U7 2020




e

Institutionalised dreams: the art of managing foreign aid / Elżbieta Drążkiewicz

Dewey Library - HC60.D686 2020




e

The passion economy: the new rules for thriving in the twenty-first century / Adam Davidson

Dewey Library - HC59.3.D38 2020




e

Failure / Arjun Appadurai and Neta Alexander

Dewey Library - HC79.T4 A66 2020




e

Quantitative economics with R: A Data Science Approach / by Vikram Dayal

Online Resource




e

Chicago price theory / Sonia Jaffe, Robert Minton, Casey B. Mulligan, and Kevin M. Murphy

Dewey Library - HB98.3.J34 2019




e

Rule Britannia: Brexit and the end of empire / Danny Dorling & Sally Tomlinson

Dewey Library - HC240.25.G7 D67 2019




e

The wolf at the door: the menace of economic insecurity and how to fight it / Michael J. Graetz and Ian Shapiro

Dewey Library - HC103.G73 2020




e

ICT Analysis and Applications: proceedings of ICT4SD 2019. / Simon Fong, Nilanjan Dey, Amit Joshi, editors

Online Resource