How fast is fast enough? A story about a 200-page PDF and a frozen tab
July 9, 2026•999 words
There's a specific kind of dread that shows up when you're building something that runs entirely in a browser tab: the moment you imagine someone dropping in the biggest, ugliest file they own, just to see what happens. For me that file was a 200-page scanned report someone had sent me during early testing, thick with embedded charts and full-page tables. I remember opening it locally and thinking, with real unease, this is going to lock the tab up, isn't it.
That fear is worth taking seriously, because it's not paranoid. A PDF isn't a lightweight format. It's a tree of objects, streams, fonts, cross-reference tables, and — in a case like that one — dozens of embedded images sitting on top of a content stream that has to be walked token by token if you want to touch the colors inside it. Do that walk on the main thread of a browser and you've just volunteered to freeze the UI for however long the parsing takes. Buttons stop responding. Scrolling stops. The whole tab looks dead, even though it's just busy. Users don't distinguish "busy" from "broken" — they close the tab.
So before I could answer "will this be fast enough," I had to answer a smaller, more honest question first: fast enough for what, exactly? I wasn't trying to win a benchmark. I didn't need PDF Dark to out-process a native desktop tool written in C. I needed one specific, narrow thing: after someone clicks convert, the tab has to stay alive. Scrolling has to keep working. The spinner has to actually spin. That's a much more achievable target than "as fast as possible," and it's the target that actually matters to a person sitting there watching a progress bar.
Getting the work off the main thread
The fix wasn't clever, and I don't think it needed to be. All the real work — decoding the PDF's object structure, walking each page's content stream, rewriting the color operators — runs inside a Web Worker instead of on the thread that's also responsible for painting the page you're looking at. The main thread's only job during a conversion is to hand the file off, wait, and update a progress indicator. Everything expensive happens somewhere the browser's renderer never has to wait on.
This sounds like an obvious move once you say it out loud, but it changes the entire feel of the tool. A 200-page file that would otherwise stall the interface for several seconds instead just runs quietly in the background while you can still scroll the page, switch tabs, or reread the instructions. The tab never looks broken, because from the browser's point of view, it isn't busy — the worker is.
Why the actual color-rewriting step stays cheap
Moving work off the main thread buys you responsiveness, but it doesn't make the work itself fast. That part came from being narrow about what actually gets touched. The tool doesn't rasterize a page and generate a new image, and it doesn't decode and re-encode the photos and charts sitting inside the file as image objects. Those get left completely alone — same bytes in, same bytes out. All the processing is scoped to the content stream's color operators: the rg, RG, g, and G tokens that set fill and stroke color for text and vector shapes. That's a small, well-defined slice of the file. Skipping image re-encoding isn't just a fidelity choice, it's also a performance one — decoding and recompressing embedded JPEGs is genuinely the expensive part of most "convert this whole PDF" tools, and PDF Dark simply never does it.
The other place I had to be deliberate is table backgrounds, because a lazy version of this feature would either invert everything (turning navy headers into some ugly inverted hue) or leave everything alone (leaving glaring white table cells on an otherwise dark page). What I landed on is a luminance check: any solid fill above roughly 0.85 luminance — the light backgrounds people use for table headers and code blocks — gets remapped to a dark tone. Anything at or below that threshold keeps its original hue and just gets darkened in place. It's a single comparison per shape, cheap to compute, and it's the difference between a converted document that still looks intentionally designed versus one that looks like a bug report.
The number that actually matters
After wiring the worker and narrowing the scope of what gets rewritten, that 200-page file that started this whole investigation converts in a few seconds, and the more typical case — something like a 50-page report — lands around two seconds, start to finish, without the file ever leaving the browser. I don't think of that number as a finish line I'm racing toward zero. Two seconds with a tab that stays fully interactive the entire time is a better outcome than half a second with a tab that's frozen and unreadable, because the second one erodes trust even if it's technically "faster."
That's really the whole lesson from working on this: performance work isn't a single dial you crank clockwise forever. It's a specific, bounded promise — the tab does not lock up — that you can actually keep, verify, and stop worrying about once it's true. https://pdfdark.org is where the current build of the tool lives if you want to see what a couple hundred pages does to it yourself; it's MIT-licensed, so the worker code and the color-rewriting logic are both sitting in the open if you want to see exactly where the two seconds goes.
I still keep that original 200-page file around as a kind of regression test. Every so often I drop it back in, mostly out of habit, and watch the tab stay calm while it works. It's a small ritual, but it's the closest thing I have to proof that the fear I started with was answered correctly instead of just quietly ignored.