In een SaaS voor beeldgeneratie verwijderde ik .prettierrc, eslint.config.mjs en zeven dependencies uit package.json in dezelfde commit. Het lockfile verloor 545 regels. De dag erna deed ik hetzelfde in een ander project, en in nog een. Bij de vijfde keer was ik niets meer aan het afwegen: ik opende de repo, haalde ESLint en Prettier eruit, en schreef biome.json.
Dat is geen voorkeur meer. Dat is een oordeel, en dan kun je het maar beter kunnen verdedigen.
Wat me de eerste keer over de streep trok
Niet de snelheid. Het was de dependency-boom. De configuratie die ik weghaalde was deze:
const compat = new FlatCompat({ baseDirectory: __dirname });
export default [...compat.extends("next/core-web-vitals", "next/typescript", "prettier")];
Een FlatCompat zodat de flat config oude configuraties begreep. Een eslint-config-prettier waarvan de enige taak is om de ESLint-regels uit te zetten die botsen met Prettier. Een eslint-plugin-tailwindcss en een prettier-plugin-tailwindcss die vergelijkbare dingen doen vanuit twee verschillende kanten. Zes pakketten die alleen bestaan omdat de andere tools elkaar niet in de weg mogen zitten.
Biome doet formatteren en linten in hetzelfde binary. Dat probleem verdwijnt omdat er geen twee tools meer zijn om op elkaar af te stemmen.
Wat er stukgaat zonder dat iemand je waarschuwt
De migratie is niet: bestanden verwijderen en doorgaan. Het is de formatteerslag die er direct op volgt: 78 bestanden aangeraakt in één commit, waarin je niets kunt reviewen omdat het allemaal aanhalingstekens en inspringing is. Je doet het en je accepteert het.
Het echte probleem komt als je de linter serieus aanzet. In een herinneringsbot op Telegram gaf de eerste ronde met de aanbevolen regels 60 fouten op de website. Bijna geen daarvan was ruis:
- Labels zonder koppeling. Loginvelden zonder
htmlFor/id. ESLint met de config van Next had me dat nooit verteld. - Decoratieve SVG's. De regel herkent de verkorte vorm van
aria-hiddenniet; je moet explicietaria-hidden="true"zetten. - Naambotsing. Een geïmporteerd type
TaskListdat botste met een component met dezelfde naam. Compileerde prima en was fout. - Keys op index. In een component met roterende tekst was de key van elk karakter de index van de
map. De oplossing was niet de regel het zwijgen opleggen: het was de absolute offset voorberekenen in de memo om een stabiele key te krijgen.
Dat is het interessante punt. Van die 60 waren de meeste echte fouten die er al maanden zaten. De rest is opgelost met gedocumenteerde onderdrukkingen: een effect dat één keer draait, loops van skeletons met vaste omvang, een !important die over react-remove-scroll-bar heen gaat, <img> van OAuth-avatars.
Een biome-ignore zonder commentaar dat uitlegt waarom is schuld. Met commentaar is het een beslissing.
Snelheid telt vanwege waar je de lint kunt zetten
Het is wat het vaakst terugkomt in vergelijkingen en wat het slechtst wordt uitgelegd. Het getal doet er niet toe. Wat telt is waar je het commando kunt inpluggen als het weinig tijd kost.
Met ESLint woonde de lint in CI, want hem in de lokale build stoppen betekende wachten. Met Biome werd het script dit:
"build": "bun run lint && next build",
"lint": "biome check .",
"lint:fix": "biome check --write ."
De lint vóór de build. Staat hij rood, dan compileer je niet. Die ene regel is het hele verschil: de fout wordt niet meer een kwartier later ontdekt in een runner, maar voordat de build begint. Met een trage tool is die regel onhoudbaar en haal je hem er uiteindelijk uit.
Wat je kwijtraakt
Twee dingen, en het is de moeite waard ze te benoemen.
Het eerste is het plugin-ecosysteem. Ben je afhankelijk van een heel specifieke ESLint-plugin, dan is die er niet en komt hij er waarschijnlijk ook niet. Ik merkte het toen ik eslint-plugin-tailwindcss weghaalde: de klassenvolgorde regelt de formatter van Tailwind, niet de linter, dus ik miste hem niet. Maar dat is geluk, geen algemene regel.
Het tweede is dommer en geeft meer gedoe: de standaardconfiguratie is niet overal hetzelfde. Eén project kwam eruit met "indentStyle": "tab" en een ander met "indentStyle": "space" en "indentWidth": 2. Weken later moest ik een commit maken die uitsluitend tabs door spaties verving in biome.json zodat alles gelijk was. Zet het $schema vast op de exacte versie en kopieer hetzelfde biome.json tussen projecten. Het gemak van deze tool is dat hij bijna geen configuratie heeft; maak daar gebruik van en houd er één.
Mijn kwaliteitsstack vandaag
Biome, TypeScript in strikte modus, tests, en knip voor dode code. Veel meer niet.
Knip is het stuk dat de minste mensen gebruiken en dat het meest verrast. Eén ronde in de Telegram-bot verwijderde 19 dode bestanden en 21 ongebruikte dependencies: @radix-ui/*-subpakketten die overbodig waren naast het metapakket, @react-three/fiber en three compleet, date-fns, en eslint, dat maanden na de verwijdering nog geïnstalleerd stond. Er kwam ook een compleet cluster acties en use cases uit die niemand meer aanriep.
Maar knip blijft bewust buiten CI. In een grote repo gaf de eerste ronde 107 al bestaande bevindingen: koppel je hem aan de gate, dan staat hij op dag één rood en uiteindelijk negeer je dat rood. Het is een handmatige tool, elke paar maanden, met het snoeiwerk in een eigen commit en geverifieerd met tsc en de tests voordat je merget.
De configuratie moet expliciet zijn over de entrypoints, anders verwijdert hij je halve applicatie. In een monorepo met Next moet je met de hand aangeven welke bestanden entrypoints zijn:
"entry": ["app/**/{page,layout,route,sitemap,robots,icon}.{ts,tsx}"],
"ignore": ["components/ui/**"],
"ignoreDependencies": ["sharp", "tailwindcss", "tw-animate-css"]
De door shadcn gegenereerde componenten gaan in ignore omdat er altijd meer zijn dan je gebruikt en dat is normaal. sharp en tailwindcss gaan in ignoreDependencies omdat niemand ze importeert; ze worden vanuit de build gebruikt.
Een trage linter is een linter die eindigt in een aparte job waar niemand naar kijkt. Een snelle linter gaat de build in en wordt de bodem van het project.
