Thoughts from 6 years of UI engineering

Published on 2020-09-12

Over the last decade, I've been fortunate to spend the last 6 building UIs for web applications professionally. The ecosystem has evolved rapidly, and navigating it can be a minefield of outdated information, illusions like "best practices" (I don't think they exist), and unnecessary debates. Personally, I think the trade-off is worth it. The Web is hands-down the best platform for rapid UI development.

I generally work on teams that use React, but this article is intended to be framework agnostic. I commonly end up building (or contributing to) reusable component libraries within a company, so the advice on infra will only be useful if you do the same.

Nothing here is presented as fact, it's what I've personally found effective when building UIs in teams. I hope my future self reflects on any incorrect assertions I've made here :)

Once this post is 1 year old, a warning will show on this page indicating that information may be outdated.

Always bet on the Web

It's important to understand that the Web is a series of hacks built on a series of hacks. Its current form is not what its founders had envisioned. Instead, it's become the most ubiquitous platform as a result of diverse ideas. The Web has expanded to cover an overwhelming amount of use cases.

Use this to your advantage. If you want to build something, someone has probably already done it before -- that means it's supported by the platform! If enough people do it, it'll likely become a standard part of the platform (remember implementing border radiuses before border-radius was in CSS?).

I find it effective to skim over lengthy documents, which plants metaphorical seeds in my brain. In the future, I may be tasked with implementing a feature that I haven't before. My hope is that I vaguely remember reading about something, and I can go back and look it up. There is comfort knowing what I don't know.

It's much better than the alternative, not knowing what I don't know.

Example: building a newspaper like text layout. In CSS, this is very easy to do, but depends on knowing that columns exists. This property is really difficult to come across unless you've heard about it before.

To have this kind of intuition, I recommend:

Skimming specs

This is a weird one. I don't like it when people to tell me, "go read the docs!" when I ask a question about software that I'm unfamiliar with. It's not a good way to learn new concepts. Specs are especially difficult because they are meant as documentation for browser vendors, not UI engineers. In order to not go insane, I suggest starting with the MDN docs for fundamental CSS properties, and following the links at the bottom to the relevant spec section.

One starting point: the docs for display. At the bottom, there's links to specs (usually W3C). If you click through, you may notice some confusing notation. Luckily, most specs have their own specs.

When reading through a spec, I open a new tab for any new links that sound interesting. I repeat this process until I'm exhausted, and hopefully I'll have a slightly crisper understanding of how a feature works. I'll also have learned some trivia that may be useful in the future.

Read YDKJS

Kyle Simpson's You Don't Know JavaScript series has been instrumental in my career. It's taught me that most programming concepts do make sense once you understand the motivations and intricacies of the language. There are other books that may work better for you, but I personally like YDKJS. You'll definitely impress a potential interviewer if you read through the books :)

You'll learn some arcane constructs that don't have everyday application, but may come in handy when you get a weird JavaScript error or import a 15-year old library. I don't know much about the with keyword, or how exactly the Temporal Dead Zone (TDZ) works, but knowing its existence helps me troubleshoot random things.

Use the platform

Native DOM APIs are quite powerful, and overlooking these causes unnecessary work. Most things in CSS are doable with 3 properties or less. When working with forms, HTML attributes are extremely powerful.

I prefer to build experiences with purely HTML/CSS, but I don't hesitate to reach for JavaScript if the interactivity is significantly enhanced. For web applications, I don't prioritize users that have JavaScript disabled, unless it has a noticeable impact on accessibility. For websites that aren't web applications, I adopt the opposite philosophy, but that's for another post.

Learn CSS

If you feel like you're writing hacks in CSS to achieve basic functionality, ask yourself if there's a better way. There usually is. If you can drop IE11 support, this is especially true. CSS is really big, so focus on finding your own personal workflow. It will become much easier. There's a lot of things that I rarely use in CSS anymore:

  • Global class names
  • The cascade
  • Combinators in general

Things that I always do when starting a new project:

Don't use the platform

The platform has unavoidable shortcomings. Mobile gets a bunch of things for free that web simply doesn't. This comes up a lot when building data-rich UIs. Whenever I need to build a virtualized list, I immediately reach for JavaScript. Depending on your framework of choice, you can reach for a lot of off-the-shelf the solutions. I recommend building your own. A good virtualized list is coupled to your data fetching strategy. If you want to achieve 60FPS, an off-the-shelf solution will run into a bottleneck. If you use Spotify, I recommend opening the DOM inspector and reverse engineering their implementation. They fetch a list of all IDs, and then fetch information for each one depending on scroll position. It's quite clever but introduces a lot of other problems that require both front-end and back-end work.

Learn web infrastructure

Infra means learning the right tools for the job, but also mistakes that you may inherit from previous engineers. These mistakes may not have been apparent during the initial implementation, so it's rarely the fault of the original implementer; just a by-product of a fast-moving ecosystem.

There's massive mind-maps out there depicting what a front-end engineer "should" know in order to be effective. I'm not a fan of these. Here is my toolkit for building out web infrastructure. There are three major components:

  • Ways to keep up-to-date with the ecosystem without being overwhelmed
  • A generic, "boring" stack that can be used for new projects that require customization. These may not be the best tools, but they are mature enough while still supporting cutting-edge features if desired.
  • Quick scaffolding tools for prototypes, or projects where the content is more important than the architecture (i.e. this website)

You should adjust it to your preferences.

Keeping up

Infra moves really fast, and new tools are easy to miss. My recommendation is to check a few resources every now and then. Here are a few that I like:

  • Check /r/javascript once a week, and sort by top posts weekly.
  • Check /r/reactjs (or /r/vuejs, etc) once a week.
  • Create a Twitter account, and follow a few open-source library authors that are actively developing libraries. They often post interesting stuff. If their posts are too negative, I suggest muting them and checking them on a weekly basis. Maintaining sanity and positivity has helped me maintain interest in the Web.
  • If you find it useful to read others' code, I recommend following a few library authors on GitHub too. They often star interesting repositories, and can be another way to find undiscovered projects.

You may also find interesting open source projects, which can be interesting to review.

Personally, I find the chat-based communities distracting. They're much more difficult to search, and they're flooded with low-effort questions that often cause people to ignore your questions, even if they're well-formed. Your mileage may vary.

A boring, yet effective stack

My preferred combination of tools for the past 3 years has been: Webpack, Babel, ESLint, Jest, PostCSS, and CSS Modules. The first 3 tools are all only as useful as their configurations, which means you need to learn the basics to be effective.

I wanted to point to specific repos because the ecosystem is quite overwhelming. It's likely that this information will go out-of-date, please keep that in mind if you're reading this from the future.

Webpack in 2 minutes

If you're just starting a project, the main webpack documentation is reasonable. You set up a few entry points, define some outputs, and you're good to go. I explicitly configure loaders for:

  • .js/.jsx/.ts/.tsx: babel-loader with babel-preset-env.

  • .css/.module.css: css-loader, style-loader, and postcss-loader, MiniCSSExtractPlugin

    • css-loader resolves CSS imports inside your CSS files. It allows CSS import/url declarations to be recognized by Webpack. Without this, imports won't work properly as webpack mangles URLs when combining/splitting modules.
    • style-loader injects CSS as style tags. This is great when using fast-refresh, it allows it to work seamlessly under the hood.
    • postcss-loader allows you to apply complex transformations that will deliver a better user experience. For example, you can apply autoprefixer, postcss-flexbugs-fixes, and even extend CSS by using postcss-preset-env.
  • .png/.jpg/.woff/.woff2/.ttf/.otf/: file-loader. I might have missed a few file extensions here; any binary files like images and font files apply here.
  • .svg: Any svg-loader that's relevant to the framework you're using. For React, I use svg-react-loader.
  • Any sort of fast-refresh setup that you need for your framework. For React, I use react-fast-refresh-plugin.

Once you've got a baseline set up, you can explore things like code-splitting, etc. I recommend getting fast-refresh working right away with all of your different modules (such as React, Redux, CSS Modules).

Another good way to learn webpack is to use a scaffolding tool. For example, you can eject from create-react-app, inspect the webpack config, and try and learn what each line does.

Babel in 2 minutes

I start with babel-preset-env, and babel-preset-react. I find myself occasionally wanting to write a Babel plugin. The most common case is resolving circular imports transparently. If you have a root file that simply re-exports a bunch of other imports, you don't want people to accidentally import files from here (this is very easy to do on accident when using auto-completion).

The advantage of writing your own plugin is that it can be customized for future optimizations. Once you get the hang of it, writing Babel plugins isn't too bad. You can usually find someone else's open-source work and fork it.

One caveat: Writing a Babel plugin utilizing newer TypeScript or ESLint features is not a great experience. These are often not adopted by parsers right away, so be prepared to submit a PR.

ESLint in 2 minutes

ESLint is the most subjective of the bunch. I've significantly reduced my dependency on ESLint in the past few years. I mostly use it for custom lint rules. I use Prettier instead to resolve any style issues, and the TypeScript compiler to catch any errors (like undefined variables) but I generally do a few things right away:

  • Use the latest ecmaVersion, and babel-eslint as the parser
  • Use eslint-config-prettier, it disables all the style rules in ESLint for you.

I mainly use ESLint when I need to enforce new patterns. For example, if we want to disallow importing a certain module when writing new components, a lint rule can be written for this.

Other tools in my boring stack

For Babel and ESLint, a basic understanding of the AST is necessary. You can copy-paste some code into AST explorer, and click through the data structure to get a basic grasp on how code is tokenized. I've written some compiler at every job, so I've found it useful to explore this a bit more.

I was lucky enough to have a Compiler Design course in university, but The Super Tiny Compiler is a fantastic crash course to compilers.

At the time of writing, I primarily use React, but I've also had great experiences with Vue. Both ecosystems are vibrant and there are tons of npm packages for both. I like experimenting with less popular frameworks in smaller projects. If you're looking to do the same, I'd recommend checking out Svelte, Crank.js, Mithril.

For websites (not web applications), I typically use Next.js. For sites that don't benefit from server-side rendering, such as this site, I use Gatsby.

If I want full control over the entire stack, my go-to tools are: Node.js, Express, PostgreSQL, and React.

If I want to build a product while learning something new, I limit myself to adding only one "new" thing. Currently, I'm experimenting with Deno, Rust, and Phoenix, but I wouldn't push these on a team yet. I would spend too much time debugging basic issues instead of building the product.

Testing is a series of trade-offs

Catching errors early has been critical to my productivity. However, it requires adding infrastructure which takes time, effort, and resources to maintain. If you have infinite resources, then you should tackle all of these problems! If not, choose your battles.

I always seek to have some process involved in each of these phases:

  • At compile-time
  • At test-time
  • At build-time
  • At run-time

Compile-time

Getting feedback while you're editing a module catches silly mistakes before they cascade into bad design choices. For example, you may not realize that you're passing null to a function that doesn't handle null properly; using TypeScript helps with this. At a simpler level, you may simply have a typo in your exports. ESLint can catch this, but TypeScript does an even better job.

Test-time

Tests warn engineers when they change to the component. An example workflow for updating a module:

  • New requirement means adding branching logic (i.e. if X is true, display new value; otherwise display old value).
  • The test fails, and the engineer sees that it expects the old value to be rendered
  • The engineer supplies the truthy condition, the new copy shows, and a test can be written to capture this case.

Tests (except for end-to-end tests) are generally run in simulated environments, so complex maneuvers. To avoid this, stick to your tools avoid mocking (as much as possible). Jest utilizes JSDOM, which supports operations that don't depend on layout calculations. Tools like testing-library make you to write tests from a user perspective, so you're less tempted to mock modules.

Build-time

When communicating with third-party APIs, it's difficult to test locally. This is a use case for mocking, but mocking doesn't give you confidence that your user-facing application is running properly.

Instead, you can run tests before code makes it to production. You may opt for a staging environment, or a canary strategy; it doesn't really matter.

Run-time

At run-time we can catch errors, but ideally this is just to catch bugs. I use Sentry for most projects, it's fairly straightforward and can be self-hosted if needed.

Know your tools

Writing code is a large part of the process, but debugging is also another part. Tools like react-refresh make a great developer experience. You can manually verify whether your changes worked or not. I've found that the most worthwhile investment is DevTools, as it's part of every web project you'll work on. Learn how to use it effectively in another blog post.


Sidd's face

I write about UI engineering, JavaScript, and other stuff. I'm based out of San Francisco, CA, currently working on the web team at Wealthfront.

Comments? Tweet at me, or shoot me an email!