Voorkom JavaScript-moeheid: minimaliseer het aantal NPM packages
Binnen modern web-development zijn er veel processen waarbij we NPM-packages gebruiken. De meest herkenbare verschijningsvormen van NPM-packages zijn libraries en frameworks, maar in de afgelopen jaren is het scala behoorlijk uitgebreid.
In veel codebases zie je ook: linters , test-engines, type-definitions en CLI-tools. Deze groei is verklaarbaar maar zorgt wel voor een stijging van het aantal dependencies binnen projecten. En dat zorgt weer voor problemen als: de welbekende dependency-hell, grote hoeveelheden onderhoudswerk en trage transpile-processen.
In deze post duiken we wat dieper in het NPM-ecosysteem en lees je hoe je bovenstaande problemen kunt voorkomen.
Het minimaliseren van het aantal NPM-packages is een effectief middel om je dagelijkse develop-werkzaamheden beheersbaar te houden. Door bewust om te gaan met dependencies voorkom je dat je project evolueert naar een onderhoudsnachtmerrie met kwetsbaarheden en conflicterende versies.
Door projecten op te knippen in kleinere projecten, native API's te gebruiken, en frequent bestaande projecten te updaten kan complexiteit worden voorkomen. Met tools zoals PNPM en Dependabot kun je de dependencies die je wél nodig hebt beter beheren.
Van React naar HTMX?
Toen ik als front-end developer begon bij developer.overheid.nl moest ik even wennen aan een bepaalde technische keuze. Er was namelijk besloten dat we de bestaande React front-end zouden refactoren naar een situatie waarbij een Django-app alle templates zou renderen. In de nieuwe situatie zouden we gebruik maken van HTMX om stukken van de HTML-pagina te refreshen om zo interactiviteit te realiseren.
Geen silver bullet
In onze casus leidde het gebruik van HTMX in sommige gevallen tot situaties waarbij front-end logica in Python code geschreven moest worden. Dit voelde soms behoorlijk gekunsteld. Inmiddels is een groot deel van de developer-community er wel over uit dat HTMX vooral erg geschikt is voor rapid-prototyping, en minder voor codebases die later in productie genomen moeten worden.
Afijn, dit artikel is geen deep-dive into HTMX, als je meer wilt weten verwijs ik je graag door naar dit artikel van contentful.
JavaScript fatigue
Waarom ik dit artikel begon met het verhaal over onze refactor naar HTMX was vanwege de achterliggende reden: mijn collega-developer wilde vooral naar HTMX toe omdat hij slechte ervaringen had met het onderhouden van alle NPM-dependencies waar de React-codebase ondertussen afhankelijk van was. Dit had geleid tot een serieus geval van Javascript-moeheid, in onze sector ook wel "JavaScript fatigue" genoemd. En ik geef hem geen ongelijk.
Onze package.json bevatte indertijd 77 packages. Daaronder hingen weer
1077 indirecte packages.
Onze package.json destijds
{
"name": "ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@commonground/design-system": "21.2.0",
"@fontsource/source-sans-pro": "4.5.11",
"@types/jest": "29.4.0",
"@types/node": "17.0.21",
"@types/react-dom": "17.0.18",
"@types/react-helmet": "6.1.6",
"change-case": "4.1.2",
"clsx": "1.2.1",
"debounce": "1.2.1",
"formik": "2.2.9",
"highcharts": "10.3.3",
"highcharts-react-official": "3.1.0",
"history": "5.3.0",
"http": "0.0.1-security",
"javascript-time-ago": "2.5.9",
"js-cookie": "3.0.1",
"moment": "2.29.4",
"moment-timezone": "0.5.40",
"query-string": "7.1.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-query": "3.39.3",
"react-router-dom": "6.8.0",
"react-select": "5.7.0",
"react-select-async-paginate": "0.7.2",
"redoc": "2.0.0",
"sass": "1.58.0",
"styled-components": "5.3.6",
"typescript": "4.9.5",
"url": "0.11.0",
"usehooks-ts": "2.9.1",
"util": "0.12.5",
"yup": "0.32.11"
},
"devDependencies": {
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.4.3",
"@types/enzyme": "3.10.12",
"@types/react": "17.0.53",
"@types/react-router-dom": "5.3.3",
"@types/styled-components": "5.1.26",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"buffer": "6.0.3",
"crypto-browserify": "3.12.0",
"enzyme": "3.11.0",
"eslint-config-prettier": "8.6.0",
"eslint-config-react": "1.1.7",
"eslint-plugin-better-styled-components": "1.1.2",
"eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-security": "1.7.1",
"eslint-plugin-standard": "4.1.0",
"https-browserify": "1.0.0",
"os-browserify": "0.3.0",
"path-browserify": "1.0.1",
"postcss-flexbugs-fixes": "5.0.2",
"postcss-normalize": "10.0.1",
"postcss-preset-env": "8.0.1",
"prettier": "2.8.3",
"process": "0.11.10",
"react-app-rewired": "2.2.1",
"react-scripts": "5.0.1",
"react-test-renderer": "17.0.2",
"stream-browserify": "3.0.0",
"stream-http": "3.2.0",
"stylelint": "14.16.1",
"stylelint-config-prettier": "9.0.4",
"stylelint-config-standard-scss": "6.1.0",
"stylelint-order": "6.0.1",
"stylelint-selector-bem-pattern": "2.1.1",
"webpack": "5.75.0"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "npm run lint:js && react-app-rewired test",
"test:coverage": "npm run test -- --coverage",
"test:coverage:changed": "npm run test:coverage -- --changedSince=origin/main",
"eject": "react-scripts eject",
"tsc": "tsc",
"lint": "npm run tsc --noEmit && npm run lint:js",
"lint:js": "eslint \"src/**/*.{ts,tsx}\" \"src/**/*.{js,jsx}\"",
"lint:fix": "npm run lint:js -- --fix",
"lint:css": "stylelint --allow-empty-input '**/*.{css,scss}'"
},
"browserslist": [">0.2%", "not dead", "not op_mini all"],
"proxy": "http://127.0.0.1:8000"
}
Veel packages, veel gedoe
Een codebase met veel packages heeft allerlei nadelen die je misschien niet direct in je achterhoofd hebt als je het installeert. Op dat moment wil je "alleen maar even iets oplossen". En dat terwijl je met elke package een afhankelijkheid creëert.
Het left-pad incident
Het meest dramatische voorbeeld om deze afhankelijkheid te illustreren is het
left-pad incident.
Duizenden prominente NPM packages waren naar aanleiding van dit voorval niet
meer te installeren omdat een developer had besloten zijn package left-pad te
verwijderen. Dit voorval legde de kwetsbaarheid van het JavaScript ecosysteem
goed bloot omdat één actor op zichzelf in staat was zand in de raderen van de
machine te strooien.
NPM: veel kleine packages
Omdat het NPM ecosysteem rijk is aan kleine packages die één dingetje voor je oplossen, loopt het aantal snel op. Achter elke package die je installeert bevindt zich weer een hele trits aan secundaire, tertiaire en soms ook quaternaire packages. Op deze manier kan een eenvoudig project snel uitlopen tot een onderhoudsdebacle.
🌊 Een golf van versiebumps
Elke keer als er een vulnerability wordt gevonden in een onderliggende dependency dienen de maintainers van de dependencies die deze dependency gebruiken hun package te updaten. Als jij deze package dan weer gebruikt levert dit jou weer een Dependabot-alert op. Zo zorgt een kwetsbaarheid voor een golf aan alerts tot bovenaan de chain.
Alle nadelen
🔒 Security risico's
Elke dependency introduceert potentiële kwetsbaarheden. Meer packages betekent een groter aanvalsoppervlak. Misschien denk je: "ach, het draait in de browser, wat kan er mis gaan?". Maar bedenk dan dat jouw development omgeving en build pipeline ook interessante doelwitten kunnen zijn voor aanvallers. Aanvallen die kunnen plaatsvinden zijn:
- Environment variables stelen (API keys, secrets)
- Source code modificeren of backdoors injecteren
- Lokale bestanden lezen en verzenden
- Je ontwikkelomgeving compromitteren
👹 Dependency hell
Conflicterende versies, broken updates, en onderhoudsproblemen stapelen zich op. Ik zal niet de enige zijn met de ervaring dat een oud JavaScript project niet meer te installeren is, laat staan te upgraden. Packages kunnen deprecated raken of incompatibel worden met elkaar, wat je project onbruikbaar maakt.
🐘 Bundle size
Elke package die je importeert vergroot je bundle size. Hoe groter je bundle size, hoe langer het duurt deze te downloaden en te parsen, en de user de pagina voor z'n neus heeft.
🤖 Onderhoudslast
Updates, security patches, en bijbehorende compatibiliteit-checks kosten tijd. In het NPM-ecosysteem waar veel packages afhankelijk zijn van andere packages heb je hier nog extra veel mee te maken.
⛓️💥 Externe Afhankelijkheid
Met elke package ben je afhankelijk van de maintainers voor onderhoud, en de beschikbaarheid van de package. Denk nogmaals aan het left-pad incident.
Hoe kan het beter?
✂️ Knip projecten op
Sommige web-apps zijn op te knippen in kleinere codebases die een specifiek doel hebben. Bij developer.overheid.nl hebben we dit bijvoorbeeld gedaan met onze OAS generator. Dit project was eerst onderdeel van een grotere codebase, maar door deze los te knippen zijn we wendbaarder geworden. Het is nu bijvoorbeeld makkelijker om deze codebase uit te faseren mocht het nodig zijn.
📂 Meerdere package.json-bestanden
Door meerdere package.json-bestanden in één repository te hanteren ontstaan er verschillende apps en voorkom je onnodige complexiteit.
Een mooi voorbeeld van een project dat precies dit doet is de NL-Design-System implementatie van Gemeente Utrecht. Binnen dit project vind je meerdere packages die allemaal hun eigen functie hebben.
💭 Wees bewust
Wees je bewust van de gevolgen die het kan hebben als je een NPM-package installeert. Elke package dient up-to-date te blijven en voegt complexiteit toe aan de codebase.
🔩 Schrijf simpele functies zelf
Als de functionaliteit die je zoekt niet al te complex is, is het het overwegen waard om zelf een functie toe te voegen.
🧰 Gebruik browser-native API's
Sommige libraries lossen een probleem op dat tegenwoordig al ingebakken zit in
de browser of in de programmeertaal NodeJS. Voorbeelden
hiervan zijn Intl.DateTimeFormat ipv date-fns of Fetch API ipv axios.
📆 Verwerk updates geregeld
Door op een gezette tijd, bijvoorbeeld elke maand, tijd vrij te maken om bestaande codebases up te daten voorkom je problemen. Door de updates steeds in kleine stapjes uit te voeren is de kans kleiner dat libraries met elkaar in de clinch komen te liggen en zijn bugs die optreden makkelijker herleidbaar.
🔍 Evalueer packages
Voor je een package installeert is het belangrijk te evalueren of deze wel gezond is. Dingen waar je op kan letten:
- Er zijn regelmatig releases
- Issues worden beantwoord en opgelost
- Er zijn meer dan 1 actieve contributers
- De documentatie is in orde
- Er zijn de nodige tests aanwezig
- Er wordt een heldere changelog bijgehouden
- Groeiende/stabiele download statistieken
Door het project te evalueren voorkom je dat je later met een dependency te maken hebt die niet meer onderhouden wordt.
Tools
🤖 Dependabot
Dependabot is een tool die codebases kan scannen en pull requests inschiet als hij een library vindt die achterloopt qua versionering. Dependabot is inmiddels geïntegreerd in GitHub en per repository apart aan te zetten. Mocht je niet vast willen zitten aan functionaliteit van GitHub kan je ook self-hosted gaan met behulp van Dependabot CLI.
📋 PNPM
PNPM is een package-manager met een aantal features die kunnen voorkomen dat je in een dependency-hell terecht komt. Deze features zijn:
- 🚫 Voorkomt "phantom dependencies" (packages die je gebruikt maar niet geïnstalleerd hebt)
- 📦 Elke package heeft alleen toegang tot eigen dependencies
- 🔒 Dwingt expliciete dependency declaraties af
- ⚡ Snelle installs
NPM-minimalisme betrachten, is kwalitatieve software bouwen
De keuze voor HTMX bij developer.overheid.nl was misschien niet de ideale oplossing, maar het onderliggende probleem - JavaScript fatigue door een grote hoeveelheid dependencies - is reëel en herkenbaar. Door het aantal dependencies te beperken tot wat je werkelijk nodig hebt, creëer je ruimte om je te focussen op wat echt telt: het bouwen van waardevolle features.
Het minimaliseren van het aantal NPM-packages is een effectief middel om je dagelijkse develop-werkzaamheden beheersbaar te houden. Door bewust om te gaan met dependencies voorkom je dat je project evolueert naar een onderhoudsnachtmerrie met kwetsbaarheden en conflicterende versies.
Door projecten op te knippen in kleinere projecten, native API's te gebruiken, en frequent bestaande projecten te updaten kan complexiteit worden voorkomen. Met tools zoals PNPM en Dependabot kun je de dependencies die je wél nodig hebt beter beheren.
