RSS Amplifier

Prompt-Led Product | For PMs Building in the AI Era · Aug 25, 2026

50,000 Words Broke My Editor. Here Is How I Fixed It.

0
Sign in to vote or save

Elena | AI Product Leader · Prompt-Led Product | For PMs Building in the AI Era

Check out the Build Series. Part 1 | Part 2 | Part 3 | Part 4 | Part 5

I built DraftKit’s collaboration workspace for newsletter writers. A 1,500-word co-written post is the average use case. So I built for that.

Then I shipped Book Projects, a Pro feature that lets creators draft full manuscripts inside DraftKit. Chapter one. Chapter two. The whole thing, shared with co-authors, up to 50,000 words per project.

The editor was the same Tiptap instance I’d been running for months. I didn’t change it. I should have.

Every builder I audit eventually hits a version of this wall. The architecture that ships is the architecture you built for use case one. When you extend to use case two, you need to ask what the first version assumed. For DraftKit’s editor, every assumption was invisible until the word count climbed.

If you’re building with Tiptap, ProseMirror, or any state-heavy rich text editor and you’re adding a long-form feature, you’re about to hit the same points of failure. I’d rather you know where they are before you hit them in production.

If you want me to look at your editor architecture before you ship the next feature, reach out at hello@elenacalvillo.com. This is one of the most common gaps in AI-built product audits.

Tiptap’s onUpdate callback fires on every editor state change. Every keystroke generates a ProseMirror transaction. If you have a database write inside onUpdate, you are calling your backend once per character typed.

At 1,500 words, that is fine. At 18,000 words, the HTML payload grows with every save call, the response time increases, and the editor starts to stutter. The Floating Action Pill toolbar in DraftKit, which tracks the active formatting at the cursor in real time, was subscribing to the same editor state that content changes triggered. Typing a word re-rendered the toolbar. Every word.

The default implementation is correct for the intended use case. It is just not the right architecture for 10x that use case. That is not a criticism of Tiptap. It is a product decision I deferred too long.

Strategic advice: The moment you extend a feature to a different scale, audit its performance assumptions. The code that is fast at 2,000 words is not the code you want running at 20,000 words.

Form-heavy apps where every field change triggers a validation pass on the entire form. API integrations where every user action fires a webhook without debounce. The symptom is always the same: the product works in demos, bogs down in production.

The cause is also always the same. The initial implementation was never wrong. It was just never stress-tested at the scale the next feature required.

When you build with Lovable or Cursor, the AI scaffolds the happy path. It writes a functional autosave. It wires up the toolbar. It does not generate the performance optimization layer, because that layer only becomes necessary at a scale the first draft cannot predict.

You ship, users push it, and the edge of the architecture shows up as lag. This is not a prompt engineering problem. It is a product decision problem, and it belongs to you.

The three fixes below are what I added to DraftKit after stress-testing Book Projects. They are not complex. They are just not what the first version of the code does.

Debounce autosave, always. Do not trigger a database write on every keystroke. A 400ms debounce means the save fires only after the user pauses. One save per pause instead of one save per character typed. The user never notices the delay.

Isolate toolbar state from content state. The Floating Action Pill should subscribe to cursor and selection changes, not to content changes. Typing should not re-render the toolbar. Moving the cursor should. These are different events in ProseMirror and they need to be treated separately.

Profile before you assume you have a problem. Open the browser DevTools Performance tab, record a typing session, and read the flame graph. It will show you exactly which component is re-rendering and how often. Do not guess at the bottleneck.

Build a separate autosave hook. If you autosave inside the same onUpdate callback that drives your UI state, you are coupling two different concerns. A separate debounced hook makes both easier to test and easier to fix.

Architect for your largest use case before you ship. When I added Book Projects, the question I should have asked first was: “What does this editor look like at 50,000 words?” One question would have saved the refactor.

If your team is about to add a long-form use case to an existing editor, I can review your current implementation before this lands in production. Email me at hello@elenacalvillo.com.

Read the original on promptledproduct.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.