FSRS Plugin for Obsidian: Rust/WASM Architecture and Performance
A spaced repetition tool for Obsidian notes must use a modern algorithm and work locally with notes as-is (without rewriting them into flashcards).
Existing Obsidian plugins stop at the SM-2 algorithm circa 1987.
Alternative solutions exist "somewhere else" β outside free software, outside Markdown-first architecture β tied to the cloud or a proprietary format.
I wrote my own because I couldn't find a suitable one.
FSRS, a computational core in Rust compiled to WebAssembly.
This article covers: WebAssembly architecture, a custom parser, a lexer, and performance benchmarks. Every query runs in hundredths of a second. Blazingly fast π¦
This is a technical article. For a step-by-step user guide, see the overview article.
Why a Fourth Spaced Repetition Plugin?
At the time of writing, three popular solutions exist in Obsidian: obsidian-spaced-repetition, obsidian-recall, and obsidian-review. All use SM-2 β an algorithm nearly 40 years old. It works, but requires roughly 30% more reviews than FSRS for the same retention level.
The main drawbacks of SM-2:
- Same interval regardless of material difficulty
- Doesn't handle breaks β resets progress after a gap
- No concept of retrievability β the probability of recalling a card right now
FSRS is a step forward. But it wasn't in Obsidian. Until now.
How FSRS Works in a Nutshell
FSRS operates on a DSR model with three parameters:
- Difficulty β how hard the material is. Range: 0β10
- Stability β memory strength in days
- Retrievability β probability of recalling the card right now
After each answer (Again / Hard / Good / Easy), the algorithm recalculates difficulty and stability. Retrievability changes continuously.
The algorithm uses 21 parameters, tuned by machine learning on millions of real reviews.
Architecture: Why Rust and WebAssembly
An Obsidian plugin is JavaScript. But FSRS requires precise floating-point calculations on every review.
I chose Rust for three reasons:
- Performance β WASM runs orders of magnitude faster than JS on numerical computations
-
Ecosystem β the
rs-fsrscrate from the open-spaced-repetition community, providing the reference FSRS implementation -
Type safety β in Rust you can't accidentally mix up
difficultyandstability
Separation of concerns:
TypeScript Rust/WASM
βββββββββ βββββββββ
β’ Obsidian API β’ FSRS computations
β’ UI / rendering β’ SQL-like syntax parsing
β’ File system β’ YAML/JSON parsing
β’ Plugin lifecycle β’ Filtering and sorting
β’ Buttons, modals β’ Card cache
TypeScript is a thin wrapper over the Obsidian API. All logic lives in WASM.
Performance: The WASM β JS Boundary
Minimizing Cross-Boundary Copying
- Cache inside WASM β filtering and sorting happen right there in Rust. Only the result (20β200 rows) crosses the boundary, not all 10,000 cards.
- Incremental updates β answering a card recalculates only one record.
metadataCache: Don't Read Files
The biggest speed gain comes from not reading files. Obsidian stores parsed frontmatter in metadataCache β an in-memory cache that updates on every note change.
The plugin checks for FSRS fields via metadataCache.getFileCache() β instant, in-memory access, no I/O. Out of 105,607 files, only those whose frontmatter already contains reviews are actually read.
For reference: the plugin's own parser processes 105k files in 16 seconds, filters out 100k, and processes 5k.
Obsidian spends ~20 minutes indexing 105k files,
and ~120 seconds on 5,000 freshly added cards.
So the fair comparison is 16 seconds for the plugin vs. 120 for Obsidian.
But Obsidian does it anyway β so it's more efficient to use the ready-made cache.
Numbers
FSRS calculation for all cards runs once during the initial
vault scan. After that, the cache lives in WASM β all subsequent
operations (table load, heatmap, single card update) work with
pre-computed data and don't depend on vault size.
| Operation | Large vault (105k files, ~5,000 cards) | Small vault (710 files, 104 cards) |
|---|---|---|
| Initial scan (FSRS for all cards) | 3.2 s | 0.04 s |
| Table load (after cache) | 0.07 s | 0.04 s |
| Heatmap | 0.02 s | 0.01 s |
| Single card update | < 0.01 s | < 0.01 s |
The difference between 5,000 and 100 cards after caching β 0.03 s. Large vault logs, Small vault logs with plugins
Every plugin action, after the initial calculation, runs in hundredths of a second.
The Bottleneck
3.2 seconds for initial load β that's the FSRS calculation for each of 5,000 cards. Runs only once.
States could be persisted to disk cache, but:
- Syncing complexity between devices (on-disk cache can go stale)
- 5,000 cards / 3.2 seconds β acceptable for real-world use
- After the first launch, subsequent plugin loads are instant β the cache is already in WASM
What's Wrong (About Trade-offs)
LIMIT in the current implementation doesn't short-circuit processing β to guarantee the first N rows by retrievability, all cards must still be evaluated.
A deliberate trade-off: a real user's vault rarely exceeds 5,000β10,000 entries; a full scan + sort takes 0.005β0.010 s.
WASM Cache Instead of Local State
The entire cache (HashMap<filePath, CachedCard>) lives inside WASM as a global variable. The plugin stores no state in TypeScript at all.
Why:
- Single source of truth β no desync between JS state and WASM computations
- Fast queries β filtering/sorting happen right where the data is
- Incremental updates β targeted commands: "update this card" / "delete this one"
Where data lives. Review progress is stored directly in a note's YAML frontmatter:
---
reviews:
- date: "2026-05-03T12:00:00Z"
rating: 2
- date: "2026-05-04T08:30:00Z"
rating: 3
---
due, stability, difficulty, and state are not stored β the WASM core computes them on the fly from the review history.
SQL-like Language for Tables
The plugin's headline feature β selecting cards for review via an fsrs-table block.
You write a SQL-like query in a markdown note and get a live table
that auto-updates with every review.
Under the hood: a custom parser built from scratch β lexer β parser β AST β evaluator
in Rust/WASM. Supports SELECT, WHERE, ORDER BY, LIMIT,
and the date_format() function.
Full breakdown in a dedicated article:
SQL-like Queries in FSRS Plugin.
No Mocks as a Consequence of Architecture
Mocks aren't needed β not because they're "forbidden," but because there's nothing to mock.
TypeScript in the plugin is a thin wrapper over the Obsidian API. All logic is in Rust/WASM. Mocking Obsidian would mean checking whether the plugin closes a <div>, or whether it called vault.read() β trivial glue not worth testing. What's worth testing: the integration between your own TypeScript and your own WASM.
So tests are split into two levels:
- Rust tests (184) β pure functions, isolated from the environment
- TypeScript tests (86) β unit tests for pure functions and TS β WASM integration tests
Integration tests start and end with your own code: a raw string, a parameter, or a call on the input (TS) β parsing (WASM) β WASM cache query (WASM) β result (TS).
CI/CD: Build, Test, Release at the Push of a Button
A GitLab CI pipeline with seven stages:
-
checkβcargo fmt,cargo clippy,cargo test -
build-wasmβwasm-pack build -
encode-wasmβ embedding WASM as base64 for the bundle -
testβ TypeScript tests (vitest) -
lintβtsc --noEmit+ ESLint -
buildβ finalmain.jsbuild -
releaseβ automatic GitHub release
Current Status
The plugin is ready to use.
Tested on Ubuntu, Windows, and Android.
Available in the Obsidian community plugin catalog.
What's already done:
- CI/CD that builds and publishes releases automatically
- Transparent storage β all data in YAML frontmatter of your
.mdfiles - TS β WASM integration tests (raw SQL, no mocks)
- Heatmap
- Russian, English, and Chinese localization
- SQL-like queries for table-based card selection
Planned:
- Gather feedback
- Polish the mobile interface
How to Install
Available in the Obsidian community plugin catalog.
- Settings β Community plugins β Browse
- Find FSRS β Install
- Enable the plugin in Settings β Community plugins
Stack
- TypeScript β Obsidian API, UI
- Rust β computational core (WASM)
- esbuild β JS bundle build
- wasm-pack β WASM build
- Vitest β TypeScript tests
- GitLab CI/CD β pipeline
Links
- Repository on GitHub
- Overview article
- SQL-like queries β dedicated article
Evgene Kopylov, 2026



















