quick-lint-js

Find bugs in JavaScript programs.

Why another JavaScript linter?

Isn't one code nanny enough?

Written by strager on

A fight between ESLint, Biome, Deno, Oxc, and quick-lint-js
JavaScript linters fight to the death.
Isolated Vectors by Vecteezy. Linter logos by their respective authors.

When people hear about quick-lint-js, they ask me one of two questions:

JavaScript linters find mistakes in your code such as run-time errors, outdated code patterns, and style issues. ESLint is the de-facto standard JavaScript linter. It is highly customizable and readily available. What's not to like about ESLint?

Here is my list:

After a discussion of my ESLint grievances, I will evaluate new JavaScript linters:

Finally, I will share my predictions:

ESLint is not for editors

ESLint is command-line first, CI second, and editor last. ESLint focuses on checking your code after you write it, not while you write it. Programmers typically run ESLint as a batch job pre-commit.

I want a JavaScript code assistant; I want to catch issues while I'm typing. I miss the days when Visual Studio had my back when I hacked away in C#:

Visual Studio showing a bug: 'richTextBox1' may be null
Visual Studio showing bugs in my WinForms C# code

While you can run ESLint in your editor, the experience isn't great. I teach JavaScript a lot, so I often make small programs showcasing different JavaScript, Node.js, or browser features. ESLint only really works inside projects, so it fights you when you're writing one-off scripts.

I use Vim. ESLint's Vim plugins are weak. The ALE plugin, for example, starts a new Node.js process each time linting occurs. ESLint takes over 150 milliseconds to just start up (on my Zen 3 5950X CPU!); ESLint can't even keep up with typing at 80 WPM! We will talk about ESLint's speed in more detail later.

When typing, you often introduce syntax errors. If you type “(” but don't write the closing “)”, ESLint breaks. ESLint should at least give a helpful error message, but it just tells me “unexpected token }”:

Typing 'console.log("hello"' into VS Code and showing the ESLint error
ESLint unhelpfully tells me that the bug is because of the “}

Because ESLint's syntax error messages are low quality, you should only use ESLint after you've tested your code. At that point, ESLint is useless to me: it only is for telling me that row_index is an evil variable name, not that I have serious bugs.

I want a tool I can recommend to beginners. Unfortunately, the ESLint Visual Studio Code extension does not bundle ESLint, so it does not work out of the box. The extension doesn't tell you what's wrong if you're missing ESLint or a config file; it silently doesn't work. ESLint in VS Code is bad for beginners.

Editor support should be a first-class feature. I want a linter which helps me code.

ESLint is plugin & config hell

Want to use ESLint to lint some JavaScript code? Great! Just run ESLint:

ESLint errors if you are missing an ESLint configuration file
ESLint fails but suggests that you run eslint --init to configure

Nope. ESLint requires you to create a configuration file, even if you want a default well-established configuration. As I mentioned earlier, this makes it annoying to use ESLint for one-off scripts I make while teaching.

Want to use ESLint with a Node.js project? Great! ESLint works out of the box. Not! You need to convince ESLint that you're in a Node.js project. By default, ESLint complains:

ESLint reporting error: 'console' is not defined (no-undef)
ESLint being confused by modern JavaScript

Want to use ESLint for ES modules? Great! But it doesn't work out of the box; you gotta configure it:

ESLint reporting errors: Parsing error: 'import' and 'export' may appear only with 'sourceType: module'
ESLint desperately trying to get your attention

Want to use ESLint for JavaScript code written after 2015 with “advanced” features like arrow functions and async functions? Great! But it doesn't work out of the box; you must configure the parser's ecmaVersion.

Want to use ESLint with a JSX/React project? Great! There's a plugin for that. But you can't just install the React plugin; you also need to configure ESLint's parser, and you should probably specify the React version too. To make matters worse, the specifics for doing these things have changed over the years, so old StackOverflow answers don't work anymore.

StackOverflow answer suggesting using babel-eslint with ESLint
Old JSX ESLint configurations don't work with ESLint 8

Want to use ESLint with a TypeScript project? Great! There's a plugin for that. But just like how installing the JSX plugin wasn't enough, installing the TypeScript plugin isn't enough. You need to replace the ESLint parser (which might mean you need to change your parserOptions), and you also want to enable some TypeScript rules. Lots of configuration.

Want to use ESLint for bug-finding only and leave formatting to Prettier? Great! There's a plugin for that. But why do you need a plugin? Shouldn't I be able to just turn off ESLint's formatting rules? Well, now most ESLint formatting rules are gone (deprecated as of ESLint v8.53.0), but a few (such as no-extra-semi and no-irregular-whitespace) remain enabled by default.

Configuring ESLint only needs to be done when you make major changes to the project, or when you're just starting the project. There is even an init tool to create an ESLint config for you. Despite this, ESLint doesn't make setup easy enough for me to recommend it to beginners.

I want my batteries included. No required config file for the basics. No plugins for the basics. Easy for beginners, please.

ESLint is slow

Anyone who has used ESLint in a big project knows that ESLint's performance sucks. I work on smaller projects, so performance shouldn't be too bad, right?

For one 15k SLOC project, ESLint takes around 950 milliseconds to lint (16,000 lines per second). The --cache option helps, bringing linting down to around 400 milliseconds if I change one file. Despite the caching, ESLint processes only 38,000 lines per second! C++ linters run at over 100,000 lines per second without caching, and C++ is notoriously uglier to deal with than JavaScript.

Why does linting speed matter to me? Half a second doesn't seem that bad at first glance. As I mentioned in the ESLint is not for editors section, I need ESLint to keep up with my typing. ESLint being slow is disorienting:

Typing 'console' into Vim with ESLint. Squigglies are much slower than typing.
ESLint squigglies cannot keep up with slow typing
(Vim ALE ESLint plugin shown)

ESLint is distracting. “But strager, why are you linting while typing? Just add debouncing so it doesn't flicker as much.” No! Unacceptable. The debouncing is less distracting, but it's still distracting! In contrast, a fast linter like quick-lint-js is not distracting to me:

Typing 'console' into Vim with quick-lint-js. Squigglies keep up with typing.
quick-lint-js squigglies keep up with fast typing

ESLint performance has been improving. eslint_d and the ESLint LSP server (hidden inside the Visual Studio Code extension) help with the latency. ESLint's core has gotten faster; I had to adjust my “over 130× faster than ESLint” messaging to just “over 90× faster”.

Despite these improvements, I want a 100× faster ESLint.

New JavaScript linters

ESLint isn't what I want in a JavaScript linter. What other options are there, and how do they address the problems with ESLint? Let's look at four ESLint alternatives:

Deno Lint

Deno Lint is a part of the Deno project. Created in March 2020, it features better performance than ESLint and integration with the rest of Deno.

Deno's mascot announcing a new version
The mascot is one of the best parts of Deno

Compatibility: Deno Lint is designed for Deno projects. While it can work for non-Deno projects, there are some problems. For example, Deno Lint fails to recognize JSX syntax in .js files; you need to rename your React files to .jsx. Deno Lint probably won't ever support frameworks such as Vue and Svelte either.

Editor integration: Deno has an LSP server that can run the linter. Editor support is not prominently advertised, but it does seem to be well-supported (including in Vim). There are caveats, though. The Visual Studio Code extension does not bundle Deno and disables itself by default outside of Deno projects, making it harder to use for beginners.

Plugin & configs: Deno Lint works out of the box with no configuration. No plugins are necessary for JSX and TypeScript. Nice!

Speed: Deno Lint's command-line interface is decently fast. Sadly, the LSP server is slow. The slowness is caused by intentional debouncing. I could fork Deno and patch it out, of course.

Deno Lint is a fine linter, but its goals differ from mine. I want a general-purpose JavaScript linter, but Deno Lint is designed for Deno devs.

Biome

Biome is a linter and formatter for JavaScript and other web languages. Biome's history is a bit weird: Biome was forked from a project called Rome in August 2023. Rome, the original project, was created in February 2020, but was rewritten from scratch in September 2021. The rewrite was based on RSLint which was created in September 2020.

Compatibility: Biome is designed to work with existing projects. As such, it supports JavaScript, JSX, and TypeScript today. Biome does not require a specific framework and will support Vue and Svelte in the future.

Editor integration: Biome has an LSP server that works in Vim with coc.nvim. In my experience, the LSP server is a bit flaky because it talks to a separate Biome daemon. Biome's Visual Studio Code extension requires Biome to be installed separately, making it harder to use for beginners.

Plugin & configs: Biome does not suffer from plugin hell for basic features. Biome also works out of the box without configs. However, Biome is opinionated by default; it disallows code such as this.icons["zoomIn"] and sometimes requires blocks inside switch statements. Therefore, I need to configure Biome to shut it up.

Speed: Biome started fast when it was a light fork of RSLint. As new lint rules have been added over time, Biome's linting performance has 50× gotten worse. In my testing, Biome's LSP server is slower than ESLint's! I'm sure some of this can be fixed, but it looks like linting performance is not a key feature of Biome.

Biome shows promise. Biome's goals seem to align with mine, and it seems to be gaining popularity among JavaScript developers. I am concerned about its poor speed and stability.

quick-lint-js

quick-lint-js is my own JavaScript linter. I started writing it in March 2020 after being annoyed by ESLint's poor editor integration and Flow being too picky.

Dusty, the quick-lint-js mascot
quick-lint-js's mascot is objectively the cutest

Compatibility: Like Biome, quick-lint-js is designed to work with any kind of JavaScript-based project. It supports vanilla JavaScript and JSX today, with experimental support for TypeScript. Vue and Svelte support will come eventually.

Editor integration: quick-lint-js was designed to work with editors from the beginning. It has first-party support for various editors, including different Vim and Emacs plugins. The Visual Studio Code extension comes with the linter bundled, simplifying installation.

Plugin & configs: quick-lint-js is like Biome and Deno Lint: Your code is correctly linted out of the box. quick-lint-js focuses on correctness issues and avoids stylistic nitpicks, making it usable in any project without configuration. However, quick-lint-js's lack of configuration means that it cannot enforce style rules in a team.

Speed: As its name implies, quick-lint-js is fast. Low latency is a goal because quick-lint-js lints as you type and does not want to be distracting. The other linters we've discussed struggle to hit 30 FPS, and quick-lint-js easily passes 1000 FPS:

LSP server benchmark comparing quick-lint-js, ESLint, Biome, and Deno. quick-lint-js is sub-millisecond; other linters are slower than 60 FPS.
quick-lint-js is very fast compared to ESLint.
Biome and Deno are slower than ESLint.

quick-lint-js isn't perfect, though; it currently cannot lint directories and does not lint files in parallel.

When I created quick-lint-js, Deno Lint and Rome/Biome/RSLint were early in development. If they were mature, I might have contributed to one of those projects (probably Deno Lint). Today, it would be hard for me to abandon quick-lint-js. Just thinking about it makes me sad, so let's move on. 😆

oxlint

Oxc's oxlint, started in February 2023, is the newest of all the linters discussed. Oxc has similar goals to Biome, but oxlint tries to be compatible with ESLint.

Compatibility: oxlint supports JavaScript, JSX, and TypeScript out of the box with no configuration. However, Oxc will not support Svelte, Vue, or HTML, making its use limited for frameworks that embed JavaScript in non-.js files.

Editor integration: Oxc has a Visual Studio Code extension that bundles the oxlint linter, meaning it works out of the box. However, it is buggy to the point of being unusable; lint warnings get stuck until you save, and squigglies are often in the wrong place, for example. oxlint's LSP server exists but is not distributed, so if you want to use oxlint from Vim or Neovim, you need to build oxlint and configure it yourself.

no-const-assign warning reported by Oxc's Visual Studio Code extension spanning multiple lines
oxlint incorrectly warns about the console.log statement on line 2

Plugin & configs: oxlint learned the same lesson as the other modern linters: work out of the box without any ugly config files or separate plugins. oxlint's default rule set seems less annoying than Biome's, so I don't feel pressured to maintain oxlint configs in my project either. oxlint does plan to support custom rules, but how these extensions are distributed remains to be seen.

Speed: oxlint claims to be very fast. Due to the bugs in the LSP server, I cannot evaluate oxlint's editor latency.

oxlint also has niceties in its command line, such as parallelizing by default, which makes oxlint useful for pre-commit hooks.

Oxc is the new kid on the block. While its claims are exciting, oxlint is definitely not ready for production yet. I am also concerned that the Oxc project itself might not last long.

The future of JavaScript linters

Biome started in February 2020. Deno Lint and quick-lint-js both started in March 2020. RSLint (merged into Biome) started in September 2020. Worldwide lockdowns sure gave people the time to start their ESLint rewrites. 😅

What about 2024 and beyond? Will the industry-standard ESLint prevail, or will Deno Lint, Biome, quick-lint-js, or oxlint dethrone ESLint? Or will even newer linters not yet created take the crown?

The different linters of today have different strengths:

I predict that some of these linters will converge. Biome might adopt an 'easy mode', removing the need for quick-lint-js. Deno might replace Deno Lint with a pre-configured ESLint. quick-lint-js might add plugins and complicated configs and eventually replace ESLint. oxlint might obsolete everything else!

What is the future of quick-lint-js? After I finish TypeScript support, there are a number of directions quick-lint-js could go in: type checking, multi-module analysis, go-to-definition, fix-its, or a better CLI. Aside from features, I think quick-lint-js needs better marketing to catch up with competitors such as Biome.