Die Extension, die mir gefehlt hat
Wenn Du schon mal mit VS Code gearbeitet hast, ist Dir Import Cost von Wix wahrscheinlich begegnet. Die Extension zeigt Dir die Bundle-Größe Deiner Imports direkt neben der Import-Zeile an:
import { debounce } from 'lodash'; // 42.1kB (gzip: 14.2kB)Über 5 Millionen Installationen. So ein Tool, das man erst vermisst, wenn es plötzlich nicht mehr da ist.
Ich bin vor Kurzem mit meinem täglichen Editor auf Zed umgestiegen. Also habe ich im Extension-Marketplace nach Import Cost gesucht. Gab's nicht. Wix hat das Problem für VS Code schon vor Jahren gelöst. Ich wollte genau das Gleiche in Zed haben. Am Ende brauchte es dafür allerdings einen komplett anderen Ansatz.
Die Wand: Zed-Extensions können nichts auf den Bildschirm malen
Zeds Extension-API ist absichtlich minimal gehalten. Extensions können Sprachsupport ergänzen und Language Server starten, aber sie können nicht beeinflussen, wie der Editor eine Datei rendert. Es gibt keinen Weg zu sagen: „Pack diesen Text an Zeile 5.“ Ich habe mich durch den Zed-Source-Code gewühlt und sogar einen TODO-Kommentar gefunden, der genau diese Lücke erwähnt.
In VS Code ist das trivial. Extensions haben über die Decorations API vollen Zugriff auf die UI des Editors. In Zed ist das unmöglich.
Dachte ich jedenfalls.
Der Hack: Inlay Hints zweckentfremden
Inlay Hints, also diese grauen Labels im Editor für inferierte Typen oder Parameternamen, sind ein Standard-Feature von LSP (Language Server Protocol). LSP ist ein Protokoll, über das Editor und ein separater Prozess miteinander sprechen, der Deinen Code versteht. Der Editor stellt Fragen wie „Welche Hints soll ich anzeigen?“, und der Server antwortet mit Labels an ganz bestimmten Positionen in der Datei.
Der interessante Punkt ist: In der Spezifikation steht nirgends, dass Hints etwas mit Typen zu tun haben müssen.
Also warum nicht einen Server bauen, der statt Typinformationen einfach Bundle-Größen zurückgibt? Der Editor fragt nach Hints, und ich antworte mit: „Setz 42.1kB (gzip: 14.2kB) ans Ende von Zeile 3.“
Zed fragt nach Hints, ich liefere Bundle-Größen zurück. Zed glaubt, es würde Typinformationen anzeigen.
Architektur: ein Rust-Launcher und ein Node.js-Gehirn
Die Extension besteht aus zwei Teilen:
Teil 1: Ein winziger Rust-Wrapper. Zed-Extensions müssen in Rust geschrieben und zu WASM kompiliert werden. Meiner lädt bei der Installation einfach einen Node.js-Server von npm herunter und sagt Zed dann, dass es ihn starten soll. So sieht die Config aus:
[language_servers.import-cost-lsp]
name = "Import Cost"
languages = ["TypeScript", "TSX", "JavaScript", "JSX"]Damit sagst Du Zed: „Für jede TypeScript- oder JavaScript-Datei starte zusätzlich diesen Language Server.“
Teil 2: Ein Node.js-Server. Wenn Zed nach Inlay Hints fragt, läuft es so ab:
- Zed fragt: „Welche Hints soll ich für Zeile 1 bis 50 dieser Datei anzeigen?“
- Der Server parst die Datei, um alle Import-Statements zu finden
- Für jeden Import baut er mit esbuild ein Bundle und misst die Output-Größe
- Er schickt die Größen zurück als Inlay Hints am Ende der jeweiligen Import-Zeile
- Zed rendert sie inline, und der Nutzer sieht die Größen direkt im Code
Die Ergebnisse werden nach Paketname plus importierten Symbolen gecacht, also werden import { debounce } und import { debounce, throttle } aus lodash getrennt behandelt. 500 Einträge, 5 Minuten TTL. Nach der ersten Berechnung kommen spätere Requests sofort aus dem Cache zurück.
Parsing: die Imports finden
JavaScript-Imports gibt es in ziemlich vielen Varianten:
import { debounce } from 'lodash'; // benannter Import
import React from 'react'; // Default-Import
import * as path from 'path'; // Namespace-Import
import 'side-effects-only'; // Side-Effect-Import
const express = require('express'); // CommonJS
const { readFile } = require('fs/promises'); // destrukturiertes requireIch verwende zum Einlesen der Datei den Parser des TypeScript-Compilers und kein Regex. Der versteht ohnehin jede Import-Variante, also laufe ich einfach durch den Syntaxbaum und ziehe mir genau das heraus, was ich brauche: Paketname, was importiert wird und die exakte Zeilenposition.
Der Server überspringt Dinge, bei denen eine Messung keinen Sinn ergibt: relative Imports, also Dein eigener Code, type-only Imports, die zur Build-Zeit verschwinden, und Node.js-Builtins.
Größen berechnen: der Re-Export-Trick
Um herauszufinden, wie groß import { debounce } from 'lodash' in Deinem finalen Bundle wäre, bundle ich es tatsächlich. Minify drüber, also Whitespace raus und Variablennamen kürzen, tree-shaking drüber, also ungenutzten Code entfernen, und dann die Größe messen.
Die ursprüngliche VS-Code-Extension macht das mit webpack. Ich habe mich für esbuild entschieden. Das ist dramatisch schneller.
Für jeden Import generiere ich ein winziges Snippet und gebe es direkt an die In-Memory-API von esbuild:
const result = await esbuild.build({
stdin: {
contents: `export { debounce } from 'lodash';`,
resolveDir: projectRoot,
},
bundle: true,
minify: true,
treeShaking: true,
write: false, // Output im Speicher behalten
platform: 'node',
});
const raw = result.outputFiles[0].contents.byteLength;
const gzipped = gzipSync(result.outputFiles[0].contents).byteLength;Wichtig ist: Es ist export { debounce } und nicht import { debounce }. Wenn ich etwas importiere und dann nicht benutze, würde der Bundler es per tree-shaking komplett rauswerfen und 0 Byte melden. Mit dem Re-Export sage ich esbuild: „Dieser Code wird gebraucht.“ Dadurch entspricht die Größe ziemlich genau dem, was später wirklich shipped wird. Noch ein kleiner Hack im Hack-Stack.
Das Flag write: false ist dabei zentral. esbuild gibt den Bundle-Output als Byte-Array zurück, statt ihn auf die Platte zu schreiben. Keine Temp-Dateien, kein Aufräumen.
Gotchas
Ein stiller Config-Bug, der alles kaputtgemacht hat
esbuild muss wissen, auf welche Plattform gezielt wird. Ich hatte zuerst platform: "browser" gesetzt, weil ich ja messe, was am Ende im Browser landet.
Das Problem: Viele Pakete haben Node.js-Dependencies. Im Browser-Modus versucht esbuild dann, sie zu polyfillen oder zu überspringen. Das Ergebnis sind 0 Byte oder komplett falsche Größen. Still und leise. Kein Fehler, einfach nur falsche Zahlen. Ich habe viel zu lange daran herumdebuggt.
Der Wechsel auf platform: "node" hat alles repariert. Klingt erstmal falsch, ist es aber nicht unbedingt. Das Platform-Flag beeinflusst vor allem die module resolution: also welchen Entry-Point esbuild aus der package.json eines Pakets nimmt und ob es versucht, Node-Builtins zu polyfillen. Die Dinge, die die Größe bestimmen, also Minification und tree-shaking, verhalten sich unabhängig von der Plattform gleich.
Nutzer installieren die Extension und sehen. nichts
Die Inlay Hints, die ich hier missbrauche, sind in Zed standardmäßig deaktiviert. Und es gibt keinen Weg, wie eine Extension Editor-Settings programmatisch ändern könnte.
Das heißt: Nach der Installation sehen Nutzer erst mal. nichts. Bis sie in die Settings gehen und Inlay Hints aktivieren. Ich habe das im README ziemlich deutlich dokumentiert, aber als First-Run-Erlebnis ist das eher mäßig. Es gibt einen offenen Feature-Request, damit Extensions Default-Settings deklarieren können. Ich freue mich sehr auf den Tag, an dem das landet und die Extension einfach direkt funktioniert.
Testing
Etwas zu shippen, das im Editor anderer Leute läuft, ist nicht ganz ohne. Wenn es kaputtgeht, störst Du direkt ihren Workflow.
Ich habe Tests geschrieben, damit ich Updates ohne Bauchschmerzen shippen kann. Eine vitest-Suite, die die wichtigsten Pfade abdeckt: Findet der Parser in all den verschiedenen Varianten die richtigen Imports? Liefert der Bundler für echte Pakete plausible Größen? Läuft der Cache wirklich ab, wenn er soll? Nicht vollständig, aber genug, damit ich bei Regression-Bugs ein Sicherheitsnetz habe.
Probier's aus, und was als Nächstes kommt
Installier die Extension im Zed-Extension-Marketplace. Such nach "Import Cost" oder installiere sie über die Command Palette. Du musst in Deinen Settings noch Inlay Hints aktivieren (⌘ + ,):
{
"inlay_hints": {
"enabled": true,
"show_other_hints": true
}
}Öffne danach irgendeine .ts-, .tsx-, .js- oder .jsx-Datei, und die Bundle-Größen sollten neben Deinen Imports auftauchen.
Ein paar Dinge würde ich gern noch verbessern. Ein persistenter Cache, der Editor-Neustarts überlebt. Und irgendwann farbcodierte Hints je nach Größe, sobald Zed gestylte Inlay Hints unterstützt.
Der Source-Code liegt auf GitHub: gcampes/zed-import-cost. Der Server ist auf npm als import-cost-server veröffentlicht. PRs sind willkommen.
Das Ganze ist mit massiver AI-Unterstützung entstanden. Für Rust, TypeScript und den Test-Code habe ich Claude über OpenCode genutzt. So baue ich Dinge inzwischen.
Danke an Wix für das ursprüngliche Import Cost. Der Ansatz ist komplett anders, aber die Idee stammt von ihnen.