NoteWeb
Over the course of making LifeLab, I learned 3 fundamental truths.
- I am very bad at naming things
- I am prone to feature bloat and like to have a batteries included
- I sometimes feel this anxiety about being completely left behind on tech, where majority of my current knowledge was learned over the last 2 years, completely deprecating the knowledge I spent a decade studying before. Does anyone care about dialogue management and span matching?
Now, as continuation of 1 and direct denial of 2, I started another note taking app project. This time the goal is to stay minimal in both LOC and functionality. I even made a claude skill called guardian of minimalism that pushes back against any feature I propose.
Lifelab is a React frontend, Rust+Postgres backend web app that allows note taking and, for some reason, python coding, blog export option, AI chats and bilingual real time voice transcription. It has become so feature rich it became one of the AI slop apps polluting github, maybe with a bit more testing, benchmarking, opinions, and documentation.
I started fishing for ideas and came upon pretext on Threads which means I am about 2 months behind Twitter.
Pretext is a javascript library for measuring text layout.
I would say, 2 years ago I could not be less interested in measuring text layout, but here we are.
It was written by my former neighbor, who also seems to be one of the engineers who made the React frontend library at Facebook. It took me several reads to even understand what's it about, and a month thinking how to use it.
Here I will offer a very dumb take and an attempt to understand a technology completely out of my expertise.
A Document Object Model (DOM) is a internal browser representation of a page.
A page has headers, paragraphs, lists, buttons, and whatever else you put on it. The browser turns all of that into a big tree of objects it can reason about. Then the browser does layout. It takes the font, the page elements, the available width, and figures out things like:
- how wide each box is
- how tall each box is
- where the line breaks go
- how many lines of text there are
Even LifeLab relies on the DOM for basically everything layout-related. I give the browser a pile of instructions like "this is a header", "this is a paragraph", "this is a code block", and then I mostly just trust that it will do the right thing.
sequenceDiagram
autonumber
participant App
participant Browser
App->>Browser: Put text into a hidden div
App->>Browser: "How tall is it?"
Browser->>Browser: lay out the text
Browser-->>App: "It is 84px tall"
The core thesis of Pretext is that you should stop using the DOM as an API for text layout whenever you can compute that layout yourself.
Instead of repeatedly asking the browser questions like "how tall is this text block?" or "where did this line wrap?", Pretext measures the text pieces once, stores those measurements, and then does the line-wrapping math in JavaScript.
Querying the browser is usually fine. The problem is that in text-heavy apps, especially React apps, we often end up asking the browser to compute layout over and over again just so we can read the answer back. Pretext's argument is that, for many cases, this is unnecessary work. If you already know the text, the font, and the width, you can often compute the layout directly instead of repeatedly rendering and measuring real DOM elements. The math itself is a bit gross, though.
flowchart LR
A[Naive way] --> B[Create real HTML box]
B --> C[Ask browser how tall it became]
D[Pretext way] --> E[Measure text once]
E --> F[Compute lines in code]
F --> G[Know height immediately]
Pretext has a bunch of impressive demos and I mostly wanted an excuse to understand it by trying some of the ideas in a real project.
I decided not to import the library directly. Instead, I opted to copy the underlying ideas instead to stay dependency free. From my codex-assited education, I ended up stealing these ideas.
- Cached text measurement. In server/static/outliner.js, measureWord() uses a canvas context and _widthCache to measure words once and reuse the result.
- Userland line wrapping. wrapText() does the wrapping in JavaScript instead of creating a real DOM text block and asking the browser how it wrapped.
- Computed text height. wrapCached() and heightOf() calculate how tall a node will be from the wrapped lines, which is exactly the kind of thing Pretext is built for.
- Layout as data. The app keeps nodes in arrays/maps (nodes, nodesById, childrenOf) and derives visible rows and heights from that data, instead of treating the DOM as the source of truth.
- Canvas rendering. The notebook view in server/templates/notebook.html + server/static/outliner.js paints most of the UI to a <canvas>, which fits the same “don’t ask the DOM to do all the text work” philosophy.
I had previously built a web outliner that used one contenteditable per row. It worked fine for the first hundred nodes or so, but as the document grew it became increasingly sluggish.
With the canvas approach, virtual scrolling, and the healing power of math, the new demo feels much snappier.
Next, I will describe the feature set of NoteWeb. It has two versions.
The first version is a single self-contained HTML file. You can download it, write notes in it, and then save a copy that includes everything you added. The file is both the app and the storage format, which is tied to my longterm quine obsession.
It is a minimal, infinitely nested outliner with some Pretext-inspired ideas baked in.
- search within the file
- tags
- a simple task system with
TODOandDONE - undo and redo
- light and dark theme
- virtual scrolling so large documents stay usable
- canvas-based rendering with a single editing overlay instead of one DOM node per row
The original use case was very simple: if I need notes in some project or folder, I can just drop an empty NoteWeb file there and start writing documentation directly inside it.
It is neat, minimal, and does not use React. 53Kb to start with.
The second version is a small Rust server app. It keeps the same outliner core, but adds persistence, multiple notebooks, and a few features that only really make sense once there is an actual backend.
NoteWeb server supports creating as many notebooks as you want just by visiting a URL slug. If the notebook does not exist yet, it gets created on first visit, which eliminates friction
This version also adds cross-notebook search, backlinks, tag pages, and a dedicated todo view that collects open tasks across all notebooks. There is a small JSON API for listing notebooks, fetching them, saving them, appending nodes, deleting notebooks or individual nodes, and toggling task state. The frontend is still basically the same idea: a single canvas-based notebook view, just now backed by a Rust server.
The use case I have in mind is very specific. If you use multi-agent tools like cmux that come with a browser, you can point a browser tab at a slug matching the current worktree, and then both you and Claude can read and write notes there.
That gives you on-demand scratchpads backed by a central server that can store, aggregate, and organize them. If something should survive beyond a single session, add a tag and it becomes much easier to find later.
An additional benefit of doing this server approach is you can create any sort of automation and tooling using python scripts that do a POST request. From pre-populating dates for journal notebook to adding tasks, all automation is external. No need for having a lua engine in the app itself.
This is my extremely small contribution to the era of agentic note-taking: a notebook you can summon from anywhere there is a browser, and drop notes into a semantically relevant place, like the current git worktree.
And as a bonus, it does all this without constantly abusing the DOM just to figure out layout.
My company policy prohibits me from sharing open souce code without previous approval. I am a fairly literal person and I love following rules, so I can and will not post any code. I do draw a line at html demos to prove a point. Maybe one day, one way or the other, I will publish everything. That being said, I do wonder the value of such policy, because if you just pasted this entire blog post to Claude or ChatGPT, it could likely one-shot something very close to the solution I have in my Github. It might not be line-per-line perfect, but it would probably have the same functionality.
Does this mean I should not even share ideas publicly? Or does it mean code, by itself, is less valuable than we think?
I personally think ideas and thoughts are infinitely more important than code these days, even when it's ideas about code. I hope public thinking will not get banned by business conduct policy.
As a P.S. I am adding two versions with prepopulated data. The first one has naive content-editable-per-line approach and it struggles at 10k lines. try collapsing a node. The other version is the optimize, single canvas + math stays snappy.

