Fastest Frontend Tooling
I tried tons of frontend tools this year in my pursuit to optimize my Developer Experience. I published an incredibly fast minimal template with sensible defaults which you can use to quickly spin up new projects: cpojer/vite-ts-react-tailwind-template
.
This is not a does-it-all starter kit. The template comes with the essential tools for frontend development with minimal sensible defaults to make it easy to use, quick to get started, and adaptable to any frameworks on top. It is tuned for performance, not just in terms of actual speed, but also to maximize the time you stay focused while writing code to increase how quickly you can ship.
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”
This post was initially published at the end of 2022 but is continuously updated with new suggestions and protips.
Technologies
Vite
Vite is the best and fastest dev server I’ve used. I was skeptical at first, but now I don’t enjoy working on any front-end project that isn’t using Vite. With esbuild under the hood, it builds whole projects faster than it takes others to hot reload a single change. While other bundlers can be made fast, Vite does it all out of the box with almost no configuration required.
If you haven’t used it, try it on a small project and don’t look back.
Tailwind
Tailwind is another project I was initially skeptical about. After a year of using it, Tailwind’s tradeoffs make sense to me. The way it abstracts styling into a system of CSS variables instead of direct CSS property assignments is genius. I recommend using it if you want sensible defaults for styling, smart composition for styles, and consistency across your project. The way it handles dark-mode styles is nice, too.
Alternatives considered:
- Emotion: Use
emotion
if the above criteria don’t apply to you, like if you have a well-defined design system at your company, or you have highly specific styling needs. - Many other CSS-in-JS Libraries. Emotion is currently my favorite one.
pnpm
pnpm
is the JavaScript package manager that won my heart. It’s a joy to use for basically anything related to package management or monorepos, it is fast, and offers escape hatches when you are running into problems with third-party packages. However, while I updated many projects to use pnpm
and recommend it to everyone, this template is not opinionated about the package manager you are using.
I’ve found that for long-running projects it’s ideal to update your dependencies often. Depending on how many automated tests there are and at which stage of development a project is in, I might even update dependencies daily. This allows me to isolate issues in third-party updates to a smaller set of changes and ensures I never fall too far behind to make updates a chore.
ESLint & Prettier
ESLint and Prettier are widely adopted and you are likely using them already. They both serve overlapping concerns around code style, and on the surface, it seems like a good idea to run Prettier within ESLint. However, upon profiling why ESLint was slow on a bunch of projects, I noticed prettier consistently taking about 50% of the total ESLint runtime. If you are formatting your documents on save, there is no point in running Prettier within ESLint at the same time.
For CI or local test runs, I now run prettier --cache --check .
and eslint --cache .
as separate commands in parallel. Both projects now support a --cache
flag to reduce the amount of work they do during local development. You should use these flags!
Despite the existence of Prettier, arguments about code style such as how to sort ES module imports still exist. Manually sorting imports wastes time, and usually leads to losing context when you are writing code and then have to navigate to the top of a file to modify your import statements. I love using the @ianvs/prettier-plugin-sort-imports
plugin which automatically sorts new imports, and works perfectly together with TypeScript’s auto-import feature. Similarly, prettier-plugin-tailwindcss
automatically sorts Tailwind classes in your code.
@nkzw/eslint-config
For ESLint, I published @nkzw/eslint-config
which you can use if the following principles resonate with you:
- Error, Never Warn: People tend to ignore warnings. There is little value in only warning about potentially problematic code patterns. Either it’s an issue or not. Errors force the developer to address the problem either by fixing it or explicitly disabling the role in that location.
- Strict, consistent code style: If there are multiple ways of doing something, or there is a new language construct or best practice, this configuration will suggest the most strict and consistent solution.
- Prevent Bugs: Problematic patterns such as
instanceof
are not allowed. This forces developers to choose more robust patterns. This configuration disallows usage ofconsole
ortest.only
so that you don’t end up with unintended logging in production or CI failures. If you want to log to the console in your production app, use another function that callsconsole.log
to distinguish between debug logs and intentional logs. - Fast: Slow rules are avoided if possible. For example, it is recommended to use the fast
noUnusedLocals
check in TypeScript instead of theno-unused-vars
rules. - Don’t get in the way: Rules that get in the way or are too subjective are disabled. Rules with autofixers are preferred over rules without them.
Brains are great at pattern matching so you’ll be able to understand similar code faster if lists are sorted. You won’t spend time thinking about where to insert code because there is only one possible location. It also reduces the potential for merge conflicts. People tend to append to lists. When two people append at the same time, the chance of conflict is higher than when you insert depending on sorting rules.
I am not opinionated about which specific order some things like ES module imports are in, what matters is there is an automatic resolution algorithm about where to insert new things so you never have to think about it.
npm-run-all
I like running all tests and checks while developing locally. npm-run-all
1 parallelizes your scripts and fails instantly if one check fails, making sure it doesn’t slow you down.
json
"scripts": {"build": "vite build","dev": "vite dev","format": "prettier --write .","lint:format": "prettier --cache --check .","lint": "eslint --cache .","test": "npm-run-all --parallel tsc:check lint lint:format","tsc:check": "tsc"}
ECMAScript Modules in Node.js
ES Modules in Node.js (ESM) are great in isolation, but the ecosystem integration and legacy third-party packages are making ESM hard to use. I spent way too much time trying to figure out how to use ESM with Node without breaking everything with cryptic error messages. Like seriously, I attempted it at least five times unsuccessfully and ran into trade-offs I wasn’t willing to make. Here are my requirements:
- Use native ESM.
- Fast JS compilation.
- Immediately restart scripts when files change.
I have since figured it out. Here is how to do it:
-
Run
pnpm add -D ts-node @swc/core
-
Add
"type": "module"
to yourpackage.json
. -
In your
tsconfig.json
, add:json"ts-node": {"transpileOnly": true,"transpiler": "ts-node/transpilers/swc","files": true,"compilerOptions": {"module": "esnext","isolatedModules": false}} -
Create
script.tsx
, runchmod +x script.ts
and execute via./script.tsx
:ts#!/usr/bin/env node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esmconsole.log('Your code goes here.'); -
As of Node.js v18.11.0,
node
supports a--watch
command that you can use to instantly restart your scripts when a file changes:ts#!/usr/bin/env NODE_ENV=development node --watch --no-warnings --experimental-specifier-resolution=node --loader ts-node/esmconsole.log('This processes instantly restarts when a file changes.');
It’s a bit unfortunate that we have to use swc
for scripts when we are already using esbuild
for the frontend. However, any esbuild
related solution was much slower in my testing.
Alternatives considered:
- Before switching to the above I used to use
ts-node-dev
but it does not support ESM orswc
properly. - I spent a few hours attempting to use
vite-node
but it doesn’t gracefully handle HTTP server restarts when doing hot module reloading and it crashes the process when it encounters a syntax error. I built a solution for the former issue but the latter is a dealbreaker because it requires manually restarting the process which is too disruptive. - Direct use of
esbuild
orswc
to bundle the script/app before running it. This solution took much longer to restart on file changes. - I tried
tsx
multiple times. It always adds a 10x performance overhead for API requests in development and I ran out of time figuring out why.
TypeScript
TypeScript (and to a lesser extent React) are de-facto standards at this point. TypeScript especially offers an incredible productivity boost to any project. I assume all new frontend projects use TypeScript unless there is a really strong reason not to, and I’m just including this paragraph for completeness and to show my appreciation that’s been ongoing since January 11th 2013. Thanks TypeScript team ❤️
VS Code Extensions
Here are four extensions that keep me in the flow state for longer, especially All Autocomplete and Error Lens:
bash
code --install-extension bradlc.vscode-tailwindcsscode --install-extension dbaeumer.vscode-eslintcode --install-extension esbenp.prettier-vscodecode --install-extension usernamehw.errorlens
If you want more VS Code protips, I wrote about my favorite VS Code extensions just recently.
Frontend development has never been more fun. I still get excited whenever someone creates a game-changing tool. Many of the above tools have impressed me with how fast and delightful they are.
Footnotes
-
Despite the name it works with all JavaScript package managers ↩