What I Learned Building VSCode Extensions
Lessons from shipping two extensions to the marketplace - the good, the bad, and the WebView.
When I set out to build my first VSCode extension, I thought it would be straightforward. TypeScript, some APIs, ship it. I was wrong in the best way possible.
I ended up building two extensions - Kanban Markdown and Pixel Art Editor - and shipping both to the VS Code Marketplace and Open VSX. Along the way I learned a lot about the extension API, WebView architecture, and what it actually takes to publish developer tools that people use. Here's the honest version.
Why VSCode Extensions
I wanted to build tools that lived where developers already spend their time. Browser-based tools are fine, but there's something about having your task board or sprite editor in the same window as your code. No context switching, no extra tabs, no separate login. You open a command palette, hit a shortcut, and you're there.
The extension API is surprisingly capable. You get access to the file system, workspace events, custom editors, sidebar panels, status bar items, and more. The hard part isn't what you can do - it's figuring out which API to use and how the pieces fit together.
The WebView Rabbit Hole
VSCode extensions have two main ways to render UI: TreeViews (the sidebar stuff) and WebViews (basically an iframe). I chose WebViews for both extensions because I needed rich, interactive interfaces. A kanban board with drag-and-drop. A pixel art canvas with drawing tools. TreeViews weren't going to cut it.
The catch? WebViews are completely isolated. They run in their own context with no direct access to the extension's state, the file system, or any Node.js APIs. Everything communicates through message passing. It's like building a micro-frontend architecture whether you wanted to or not.
// Extension side - sends data to the WebView
panel.webview.postMessage({ type: 'update', data: newState });
// Extension side - receives messages from the WebView
panel.webview.onDidReceiveMessage((message) => {
if (message.type === 'save') {
writeFileSync(filePath, message.content);
}
});
// WebView side - listens for extension messages
window.addEventListener('message', (event) => {
const message = event.data;
if (message.type === 'update') {
setState(message.data);
}
});
// WebView side - sends messages back to the extension
vscode.postMessage({ type: 'save', content: currentState });This architecture forces you to think carefully about your data flow. Every interaction that touches the file system or workspace has to cross the message boundary. For Kanban Markdown, that meant every card drag, every edit, every status change needed to round-trip through the extension host. I had to batch updates and debounce saves to keep things feeling responsive.
For Pixel Art Editor, the challenge was different. Drawing generates a lot of events - mouse moves, color changes, tool switches. Sending every pixel change through message passing would have been painfully slow. Instead, I kept the canvas state entirely in the WebView and only sent the final image data when the user saves. The extension side handles file I/O, the WebView handles rendering. Clean separation, but you have to be intentional about it.
State Persistence is Tricky
Here's something the docs mention but don't emphasize enough: VSCode can kill your WebView at any time to save memory. When the user switches to another tab and comes back, your entire React app remounts from scratch. All component state is gone. If you didn't persist it, it's lost.
You need to handle three scenarios:
I ended up using getState() and setState() from the WebView API for UI state (scroll position, selected card, panel sizes), and the actual file system for data state (card content, canvas pixels). File watchers on the extension side detect external changes and push updates to the WebView when it comes back.
// Save state when the WebView is about to be hidden
window.addEventListener('beforeunload', () => {
vscode.setState({
scrollPosition: container.scrollTop,
selectedCardId: selectedCard?.id,
panelWidth: sidePanel.offsetWidth,
});
});
// Restore state when the WebView remounts
const previousState = vscode.getState();
if (previousState) {
container.scrollTop = previousState.scrollPosition;
selectCard(previousState.selectedCardId);
sidePanel.style.width = previousState.panelWidth + 'px';
}The trickiest edge case was concurrent edits. Someone could have the kanban board open in a WebView, edit a markdown file directly in the text editor, and expect both views to stay in sync. File watchers handle this, but you need to be careful about circular updates - a file change triggers a WebView update, which triggers a save, which triggers a file change, and so on. Debouncing and change origin tracking solved it, but it took a few iterations to get right.
Building the WebView UI
For both extensions, I used React inside the WebView with Vite as the bundler. The WebView is essentially a sandboxed browser environment, so you can use any frontend framework you want. The setup looks like this:
@vscode/webview-ui-toolkit for components that match VSCode's native lookOne gotcha: Content Security Policy. WebViews enforce strict CSP by default. Inline scripts, external resources, and certain eval patterns are blocked. You need to generate a nonce for each WebView session and include it in your script tags. Vite's build output needs post-processing to inject these nonces.
For Pixel Art Editor specifically, the HTML Canvas API was the obvious choice for rendering. But I needed to handle zoom, pan, pixel grid overlays, and tool previews - all while keeping 60fps on large canvases. The solution was a layered canvas approach: one canvas for the actual pixel data, one for the grid overlay, and one for the tool preview cursor. Only the layers that changed get redrawn on each frame.
The Marketplace Experience
Publishing to the VSCode Marketplace was smoother than I expected. The vsce CLI packages your extension into a .vsix file, and you upload it through the publisher dashboard. Review usually takes less than a day, sometimes just a few hours.
What surprised me was the discoverability. I didn't promote either extension anywhere initially - just published and moved on. Within a week, Kanban Markdown had organic installs. People search the marketplace for specific problems ("kanban vscode", "pixel art editor"), and if your extension solves that problem, they find it.
Open VSX was an unexpected bonus. It's the open-source alternative marketplace used by VSCodium, Gitpod, Eclipse Theia, and other editors. Publishing there is a separate process but straightforward. I got more downloads from Open VSX than I expected, especially from the Gitpod and self-hosted crowd.
Some things I wish the marketplace did better:
Debugging Tips That Saved Me
A few things that made development significantly less painful:
Developer: Open Webview Developer Tools command.** It opens Chrome DevTools for your WebView. You can inspect elements, set breakpoints, and profile performance just like a regular web app.console.log from the WebView, and see everything in one place.vscode.workspace.fs instead of Node's fs** for file operations. It handles remote workspaces, virtual file systems, and permission issues that the Node API doesn't know about.What I'd Do Differently
Looking back, a few things I'd change:
vscode.d.ts typings directly - they're heavily documented with JSDoc.@vscode/test-electron runner. I skipped this initially and regretted it.Both extensions are open source if you want to see the code. The architecture isn't perfect - there are things I'd restructure if I started over - but they work, they have users, and I learned a lot building them.