gcampes
Back

Como eu levei o Import Cost pro Zed fingindo inline decorations

Apr 13, 2026 04/13/2026

zedextensionslsptypescriptrustesbuildimport-cost

A extensão que fez falta

Se você já usou VS Code, provavelmente já trombou com o Import Cost, da Wix. Ele mostra o tamanho no bundle dos seus imports ali do lado da linha do import:

import { debounce } from 'lodash';  // 42.1kB (gzip: 14.2kB)

São mais de 5 milhões de instalações. Daquelas ferramentas que você nem lembra que existem. Até o dia em que somem.

Recentemente eu troquei pro Zed como editor principal. Quando fui caçar o Import Cost no marketplace de extensões, não tinha. A Wix resolveu isso pro VS Code faz anos. Eu queria a mesma coisa no Zed, mas aí descobri que o caminho era outro. Bem outro.


A parede: extensões do Zed não conseguem desenhar na tela

A API de extensões do Zed é minimalista de propósito. Extensões podem adicionar suporte a linguagens e rodar language servers, mas não podem mexer em como o editor renderiza um arquivo. Não existe uma forma de falar "coloca esse texto na linha 5". Eu fui fuçar o código-fonte do Zed e achei até um comentário TODO reconhecendo essa lacuna.

No VS Code, isso é trivial. Extensões têm acesso total à UI do editor via a Decorations API. No Zed, não dá.

Ou pelo menos parecia que não dava.


O hack: reaproveitando inlay hints

Inlay hints, aquelas etiquetinhas cinzas que o editor usa pra mostrar tipos inferidos ou nomes de parâmetros, são uma feature padrão do LSP (Language Server Protocol). O LSP é um protocolo que deixa editores conversarem com um processo separado que entende do seu código. O editor faz perguntas ("que hints eu devo mostrar?"), e o servidor responde com labels posicionadas em pontos específicos do arquivo.

E tem um detalhe aqui: em nenhum lugar da spec diz que esses hints precisam ser sobre tipos.

Então e se eu montasse um servidor que, em vez de devolver informação de tipo, devolve tamanho de bundle? O editor pede hints, e eu respondo com "coloca '42.1kB (gzip: 14.2kB)' no fim da linha 3".

O Zed pede hints, eu devolvo tamanhos de bundle. Ele acha que tá mostrando informação de tipo.


Arquitetura: um launcher em Rust e um cérebro em Node.js

A extensão tem duas partes:

Parte 1: um wrapper minúsculo em Rust. Extensões do Zed precisam ser escritas em Rust e compiladas pra WASM. A minha só baixa um servidor Node.js do npm no momento da instalação e manda o Zed iniciá-lo. A config é esta:

[language_servers.import-cost-lsp]
name = "Import Cost"
languages = ["TypeScript", "TSX", "JavaScript", "JSX"]

Isso diz pro Zed: "pra qualquer arquivo TypeScript ou JavaScript, sobe também esse language server."

Parte 2: um servidor Node.js. Quando o Zed pede inlay hints, o fluxo é este:

  1. O Zed pergunta: "Que hints eu devo mostrar pras linhas 1-50 deste arquivo?"
  2. O servidor faz o parse do arquivo pra encontrar todos os imports
  3. Pra cada import, ele faz um bundle com esbuild e mede o tamanho da saída
  4. Ele devolve os tamanhos como inlay hints posicionados no fim de cada linha de import
  5. O Zed renderiza isso inline e a pessoa vê os tamanhos direto no código

Os resultados ficam em cache por nome do pacote + símbolos importados. Então import { debounce } e import { debounce, throttle } de lodash são tratados separadamente. São 500 entradas com TTL de 5 minutos. Depois do primeiro cálculo, as próximas requisições voltam na hora.


Parse: encontrando os imports

Imports em JavaScript aparecem de vários jeitos:

import { debounce } from 'lodash';           // import nomeado
import React from 'react';                    // import default
import * as path from 'path';                 // import de namespace
import 'side-effects-only';                   // import só por efeito colateral
const express = require('express');            // CommonJS
const { readFile } = require('fs/promises');   // require com destructuring

Eu uso o parser do compilador do TypeScript pra ler o arquivo em vez de regex. Ele já entende todos esses formatos, então eu caminho pela syntax tree e extraio o que interessa: o nome do pacote, o que tá sendo importado e a posição exata da linha.

O servidor pula o que não faz sentido medir: imports relativos (o seu próprio código), imports só de tipo (somem no build) e builtins do Node.js.


Calculando os tamanhos: o truque do re-export

Pra saber quanto import { debounce } from 'lodash' colocaria no seu bundle final, eu literalmente faço o bundle. Minify nele, tree-shake nele, e meço o resultado.

A extensão original do VS Code usa webpack pra isso. Eu fui de esbuild, que é absurdamente mais rápido.

Pra cada import, eu gero um snippet minúsculo e mando direto pra API em memória do esbuild:

const result = await esbuild.build({
  stdin: {
    contents: `export { debounce } from 'lodash';`,
    resolveDir: projectRoot,
  },
  bundle: true,
  minify: true,
  treeShaking: true,
  write: false,        // mantém a saída em memória
  platform: 'node',
});
 
const raw = result.outputFiles[0].contents.byteLength;
const gzipped = gzipSync(result.outputFiles[0].contents).byteLength;

Repara que é export { debounce }, e não import { debounce }. Se eu importasse sem usar, o bundler ia fazer tree-shake e jogar isso fora, reportando 0 bytes. Fazendo re-export, eu digo pro esbuild: "esse código é necessário". Aí o tamanho reflete de forma fiel o que iria pro deploy. Mais um hack na pilha.

A flag write: false é a peça-chave. O esbuild devolve a saída do bundle como um array de bytes em vez de escrever no disco. Sem arquivo temporário. Sem cleanup.


Pegadinhas

Um bug silencioso de config que quebrou tudo

O esbuild precisa saber qual plataforma você tá mirando. Eu coloquei platform: "browser", já que estou medindo o que vai parar no browser.

Só que muitos pacotes têm dependências de Node.js. No modo browser, o esbuild tenta fazer polyfill ou ignorar essas dependências, o que gera 0 bytes ou tamanhos completamente errados. Em silêncio. Sem erro nenhum, só número errado. Eu perdi tempo demais debugando isso.

Trocar pra platform: "node" resolveu tudo. Pode parecer esquisito, mas essa flag de plataforma afeta principalmente a resolução de módulos: qual entry point o esbuild escolhe do package.json do pacote e se ele tenta fazer polyfill de builtins do Node. As partes que determinam o tamanho de fato, como minification e tree-shaking, funcionam igual independentemente da plataforma.

A pessoa instala a extensão e não vê nada

Os inlay hints que eu tô reaproveitando vêm desativados por padrão no Zed. E não existe jeito de uma extensão mudar configurações do editor programaticamente.

Então, depois de instalar, a pessoa vê... nada. Até entrar nas configurações e ativar os inlay hints. Eu deixei isso bem destacado no README, mas continua sendo uma primeira experiência meio sofrida. Existe uma feature request aberta pra permitir que extensões declarem configurações padrão. Tô doido pra isso sair, porque aí a extensão simplesmente funciona de cara.


Testes

Publicar algo que roda dentro do editor de outra pessoa é coisa séria. Se quebrar, você tá bagunçando o fluxo de trabalho dela.

Eu escrevi testes pra conseguir soltar updates sem ansiedade. Uma suíte com vitest cobrindo os caminhos principais: o parser encontra os imports certos em todos os formatos? O bundler gera tamanhos razoáveis pra pacotes reais? O cache realmente expira as entradas? Não é exaustivo, mas já me dá uma rede de segurança boa contra bug de regressão.


Testa aí, e o que vem depois

Instale pelo marketplace de extensões do Zed. Procure por "Import Cost" ou instale via command palette. Você vai precisar ativar os inlay hints nas configurações (⌘ + ,):

{
  "inlay_hints": {
    "enabled": true,
    "show_other_hints": true
  }
}

Abra qualquer arquivo .ts, .tsx, .js ou .jsx e os tamanhos de bundle devem aparecer ao lado dos seus imports.

Tem algumas coisas que eu ainda quero melhorar. Um cache persistente que sobreviva ao restart do editor. E, mais pra frente, hints com cores baseadas no tamanho, quando o Zed passar a suportar inlay hints com estilo.

O código-fonte tá no GitHub: gcampes/zed-import-cost. O servidor tá no npm como import-cost-server. PRs são muito bem-vindos.

Isso aqui foi feito com bastante ajuda de IA. Eu usei Claude via OpenCode pro código em Rust, TypeScript e pros testes. É assim que eu construo as coisas hoje.

Valeu à Wix pelo Import Cost original. A abordagem é completamente diferente, mas a ideia é deles.