Building a custom RSVP engine, 12-format document parser, procedural ambient audio, and reading analytics. All in the browser, no account required.
March 27, 2026 · 10 min read
Speed reading tools have been around for decades, but the good ones always seemed to come with a catch. Either they required an account. Or they stored your documents on a server. Or they had one killer feature — speed, or privacy, or document format support — but nothing else. I wanted to build the version of this tool that I'd actually use: local-first, no account, no server, but with everything you'd want in a serious reading environment.
That's Speeedy. It uses Rapid Serial Visual Presentation (RSVP) — the technique where words flash one at a time in a fixed position — to eliminate the eye movements that slow conventional reading down. It went from idea to v1.0.0 in one clean release cycle.
When you read a line of text the conventional way, your eyes don't glide smoothly across it. They jump in short bursts called saccades, land, process a few words, then jump again. Those movements and the fixation pauses between them account for a significant chunk of total reading time.
RSVP removes that entirely. One word appears at the center of your screen. The next word replaces it. Your eyes don't move. You just read.
The tricky part is making it feel natural rather than like being pelted with words. That's where the engine design matters.
The core of Speeedy is a custom playback engine in rsvp-engine.ts. I didn't want to rely on a simple setInterval to flash words at a fixed rate, because that ignores everything that makes reading feel human.
The engine is token-based. Every word goes through an ORP (Optimal Recognition Point) computation, which splits it into three segments: the letters before the pivot, the pivot letter itself, and the letters after. The pivot always lands at the same horizontal position on screen. So your eye already knows where to focus before the next word arrives — the brain just has to process, not locate.
Timing is where most of the work lives. The base WPM gets adjusted per-word based on several signals:
The result is that 400 WPM through a novel feels nothing like 400 WPM through a technical paper, because the engine is reading the text, not just its word count.
There's also live rescheduling. If you adjust speed with the arrow keys while a session is running, the engine cancels the pending timeout and reschedules the next word with the updated timing immediately. No jitter, no dropped words.
One of the things I was most determined to get right was format support. Speed reading is only useful if you can actually get your content into the reader. I wanted to handle everything without ever sending a file to a server.
The parsing pipeline in text-parser.ts handles:
pdfjs-dist — but not naively. PDFs don't have paragraphs or lines at the format level. They have glyphs with X/Y positions and font metrics. I reconstruct readable prose by grouping glyphs into lines using their vertical positions, then grouping lines into blocks using spacing gaps.mammoth — straightforward, but I handle the mammoth worker loading carefully to avoid blocking the main thread.jszip — EPUB files are ZIP archives containing an OPF manifest that lists chapters in spine order. I parse the OPF, walk the spine, extract each chapter's HTML, strip tags, and concatenate them.content.xml.The parser returns a normalized document object with a word count, a title derived from the filename or embedded source, and cleaned text ready for the engine. Every format gets user-facing error messages for encrypted, invalid, or unsupported files rather than silent failures.
Speeedy stores everything locally. No account. No sync endpoint. No server.
The storage layer uses idb (a thin, typed IndexedDB wrapper) with a database named speeedy-db. There are two stores: one for the user profile (settings, stats, streak, benchmark results), one for the document library (up to 20 documents with progress tracking).
A few things I'm glad I got right early:
SHA-256 content hashing for deduplication. If you upload the same PDF twice, the second upload refreshes the save timestamp instead of duplicating the text. The hash is computed in the browser using the Web Crypto API.
Schema versioning with multi-tab safety. IndexedDB upgrades can deadlock if multiple tabs have the database open. The storage service handles the blocked and versionchange events so upgrades don't corrupt data when the user has Speeedy open in more than one tab.
Default profile creation on first launch. When Speeedy boots for the first time, it creates a default profile with sensible settings, initializes font size based on screen size, and marks onboarding as unseen so the first-run flow triggers.
Documents store their resume position as a word index. When you reopen a document you've read partway through, the reader asks if you want to resume or start over, then rewinds a few words before the saved position so you have context.
This was important to me. Reading tools often get built for the average case and ignore everyone else. Speeedy has:
Dyslexia mode — switches to OpenDyslexic styling with increased letter spacing and adjusted line height. Usable from the settings panel or from the first-run onboarding step.
Irlen overlays — a configurable color tint over the reading area. Irlen syndrome (also called scotopic sensitivity) causes some readers to experience visual stress or distortion with high-contrast black text on white. A colored overlay reduces that stress. The color is user-configurable.
Bionic reading — bold anchors on the first half of each word to give the eye a landing point. The "bionic focus position" (how much of the word gets bolded) is configurable.
ORP guide markers — small fixed markers on screen that show the reader where the pivot letter will always appear, so there's no visual searching between words.
Peripheral context — ghost words before and after the current word, with configurable count. These don't require eye movement to perceive, but they give the brain orienting information about what's coming and what just passed.
RTL support — Arabic and Hebrew words require different ORP alignment. The pivot compensation uses measured DOM offsets rather than CSS text-alignment, because the visual center of a right-to-left word is not the same as its logical center.
The audio system was one of the most technically interesting parts of the project to build.
The ambient noise generator produces white, pink, or brown noise entirely in the Web Audio API — no audio files anywhere in the codebase. The noise is generated procedurally into pre-computed AudioBuffers, then looped with crossfade smoothing at the loop boundaries to prevent audible clicks when the buffer wraps. The crossfade is short — just long enough that the seam is invisible, even when the buffer has been playing for a long time.
Per-word click sounds are also procedurally generated. The pitch is configurable, and different events (sentence endings, commas) get different pitches so reading has a subtle rhythmic texture that helps with focus without being distracting.
Audio contexts are suspended automatically when the browser tab loses focus and resumed on visibilitychange, pageshow, and focus events. This matters because browsers aggressively suspend Web Audio contexts when pages aren't visible, and recovering from a suspended context without user interaction requires handling the resume carefully.
Speeedy is designed around the idea that reading is a skill you improve over time, not just a utility for getting through documents.
Every reading session records: words read, duration, WPM, completion percentage, and source title. This feeds into:
The benchmark system gives the reading speed numbers something to anchor against. You read a timed passage at your own pace, then answer 10 comprehension questions. The result — baseline WPM and comprehension percentage — gets stored in your profile and can be used to calibrate the reader's starting speed. It also shows up on the stats dashboard so you can see how your reading speed compares to your comfortable baseline.
The share feature was a constraint-led design: no backend, no database, no accounts, but still shareable.
Profile share cards are rendered to PNG using html-to-image, which walks the DOM and generates a canvas snapshot. The PNG is downloadable.
Share URLs encode a payload (display name, WPM stats, streak, avatar) as URL-safe base64 appended to the hash. The share route decodes the hash client-side and renders the profile card. No server ever sees the data.
I have 139 unit tests passing across four test files:
Six Playwright E2E suites cover: the marketing landing page, the app intake flow, the reader playback and keyboard shortcuts, the benchmark test, settings persistence, and the full keyboard navigation model.
The CI pipeline on GitHub Actions runs biome lint → Vitest unit tests → Vite build on every push.
The Lit custom element model worked well for this project — the component boundaries are clear, the service layer is clean, and the build output is small. But Lit's reactivity model requires careful thought about when to use @property vs @state vs a signal-like pattern for cross-component state. I had a few bugs early on where profile updates in a child component didn't propagate correctly to sibling components. The solution (custom events on the document + a root-level listener in the app shell) works, but it's more boilerplate than I'd like.
The parsing layer could use a worker thread for large PDFs. Currently, parsing a 300-page PDF blocks the main thread for a few hundred milliseconds. It's not catastrophic — there's a loading state — but moving it to a Web Worker would make the intake screen feel more responsive.
Speeedy is live at speeedy.pages.dev. v1.0.0 shipped on 2026-03-27. The source is on GitHub under MIT.
It's the kind of tool I wanted to exist and couldn't find: a serious speed-reading environment that respects your privacy, works with the documents you already have, helps you track your progress over time, and gives you enough customization to actually make reading comfortable — without asking you to create an account first.