home

Spaced Repetition Anywhere

July 20, 2025 ❖ Tags: writeup, programming, webdev, javascript, artifical-intelligence

Spaced repetition1 is an effective habit for memorizing small fragments of information for, effectively, an indefinite amount of time. The idea is to repeatedly challenge your recall of a piece of information, typically with a sort of flashcard, doing so at a frequency determined by your previous recall performance: new information is challenged frequently until you remember it, and previously learned information is challenged less frequently. The concept is simple enough to implement with a few index cards and a shoebox. Despite the simplicity, the landscape of software implementations of spaced repetition is relatively small. The venerable Anki is the best-known spaced repetition application, and I've used org-drill and org-fc in the past, but other than those three, I'm not aware of many other programs for it. These existing systems are fine, but I have a fairly specific use-case that none are appropriate for: I might want to do my reviews on a system where I'm unable to install Anki or GNU Emacs, and I don't want to have to trust a third-party with my cards. This rules out services like Quizlet and AnkiWeb. It's a simple enough concept, so I wanted to see if I could write a flash card application contained within a single HTML file. All we really need is a spaced repetition scheduler algorithm, a way of implementing the "challenge" piece in the browser, and a way of storing cards and their scheduling parameters across reviews.

The Algorithm

The best-known algorithm for spaced repetition is SuperMemo 2 (SM-2), developed by Piotr Woźniak in 1990 for SuperMemo and used in Anki. SM-2 works well, but there are better scheduling algorithms available these days. My current favorite is the Free Spaced Repetition Scheduling (FSRS) algorithm because it empirically outperforms the others by a significant margin. For the theory behind it, I would recommend Domenic Denicola's article Spaced Repetition Systems Have Gotten Way Better. For our purposes, we can just treat the scheduling algorithm as a black box. Given some parameters on the card (in the case of FSRS: retrievability, stability, and difficulty) and a grade based on how well we recalled some information, we generate new parameters and a time in the future when we will next review the card.

Treating the algorithm as a black box, I took Fernando Borretti's implementation in his article Implementing FSRS in 100 Lines, dumped it into Qwen3-30B-A3B-UD-Q3_K_XL2, and asked it to translate the code to JavaScript. It made a few mistakes around the use of enums, but I was able to get it working with a handful of changes.

/***********************************************************************
 * Actual FSRS5 algorithm                                              *
 ***********************************************************************/

const W = [
  0.40255, 1.18385, 3.173, 15.69105, 7.1949, 0.5345, 1.4604, 0.0046, 1.54575, 0.1192, 1.01925,
  1.9395, 0.11, 0.29605, 2.2698, 0.2315, 2.9898, 0.51655, 0.6621,
];

const F = 19.0 / 81.0;
const C = -0.5;

const Grade = {
  forgot: 1.0,
  hard: 2.0,
  good: 3.0,
  easy: 4.0
};

function retrievability(t, s) {
  return Math.pow(1.0 + F * (t / s), C);
}

function interval(r_d, s) {
  return (s / F) * (Math.pow(r_d, 1.0 / C) - 1.0);
}

function s_0(g) {
  switch (g) {
  case Grade.forgot: return W[0];
  case Grade.hard: return W[1];
  case Grade.good: return W[2];
  case Grade.easy: return W[3];
  }
}

function s_success(d, s, r, g) {
  const t_d = 11.0 - d;
  const t_s = Math.pow(s, -W[9]);
  const t_r = Math.exp(W[10] * (1.0 - r)) - 1.0;
  const h = g === Grade.hard ? W[15] : 1.0;
  const b = g === Grade.easy ? W[16] : 1.0;
  const c = Math.exp(W[8]);
  const alpha = 1.0 + t_d * t_s * t_r * h * b * c;
  return s * alpha;
}

function s_fail(d, s, r) {
  const d_f = Math.pow(d, -W[12]);
  const s_f = Math.pow(s + 1.0, W[13]) - 1.0;
  const r_f = Math.exp(W[14] * (1.0 - r));
  const c_f = W[11];
  const result = d_f * s_f * r_f * c_f;
  return Math.min(result, s);
}

function stability(d, s, r, g) {
  return g === Grade.forgot ? s_fail(d, s, r) : s_success(d, s, r, g);
}

function clamp_d(d) {
  return Math.min(10.0, Math.max(1.0, d));
}

function d_0(g) {
  return clamp_d(W[4] - Math.exp(W[5] * (g - 1.0)) + 1.0);
}

function difficulty(d, g) {
  return clamp_d(W[7] * d_0(Grade.easy) + (1.0 - W[7]) * dp(d, g));
}

function dp(d, g) {
  return d + delta_d(g) * ((10.0 - d) / 9.0);
}

function delta_d(g) {
  return -W[6] * (g - 3.0);
}

Borretti's article provided a simulator which was helpful for testing whether the translated code was actually correct.

The Boring Part: A User Interface

Even though I spend a lot of time on building my personal website, I find many aspects of web development to be uninteresting. The idea for this self-contained spaced repetition software is one I've had for over a year now, but I've consistently put it off because I couldn't motivate myself to spend my free time tackling the step of "create a user interface in HTML, CSS, and JavaScript". Not because it's especially hard, but because I'd just rather do something else. Fortunately, resources on web development are prevalent online, I'm building something from fragments that almost certainly appear verbatim in the training data of large language models, and I just want the bare minimum that works, so this is a perfect use-case for vibe coding. Qwen did a decent job at translating the FSRS algorithm into JavaScript, and it did quite well building up the UI for me. I ended up completely scrapping the actual flashcard piece it generated, but I kept the other components of the UI with relatively minor changes. Overall, I'm happy with the results.

spaced-repetition-anywhere-browser-screenshot.png
Figure 1: SRS Anywhere in LibreWolf showing the card for 难以置信 from my HSK7-9 deck.

With some minor changes, I got it to work on mobile, too.

spaced-repetition-anywhere-mobile-screenshot.png
Figure 2: SRS Anywhere in Fennec showing the card for 就 from my HSK1 deck.

Data Model

I'm familiar with IndexedDB and other in-browser storage mechanisms, but I wanted the cards and their scheduling metadata to be portable, and to persist through a browser refresh, so I went with storing cards and metadata in a JSON file. The program loads in the cards initially from a JSON file, conducts the review, and then prompts the user to download a JSON file containing all of the cards and their updated scheduling metadata. I wrote the parsing and review logic by hand, but asked Qwen to come up with this snippet for the final piece of giving the updated cards back to the user. It saved me from having to figure out that the Blob API is what I wanted here, and then reading through the MDN article on figuring out how to use it.

function downloadJSON(obj) {
  // Convert the object to a JSON string (with default formatting)
  const jsonString = JSON.stringify(obj, null, 2); // 'null, 2' adds indentation for readability

  // Create a Blob with the JSON content
  const blob = new Blob([jsonString], { type: 'application/json' });

  // Generate a URL for the Blob
  const url = URL.createObjectURL(blob);

  // Create an anchor element
  const a = document.createElement('a');
  a.href = url;
  a.download = 'data.json'; // Default filename

  // Append the anchor to the body (required for some browsers)
  document.body.appendChild(a);

  // Simulate a click on the anchor to trigger the download
  a.click();

  // Clean up
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

The cards.json file is an array of objects containing at least the keys id, front, and back. The only requirement for id is that it's unique across the deck: it could be a UUID, but it could also be something more descriptive. front and back are HTML fragments that are injected into the page when the card is shown.3

Conclusions

It felt empowering to do all of the code generation on my Intel Arc A770 using free software. The experience alleviated some of my skepticism around the usefulness of LLM's for programming use-cases, even if it required some corralling. I'll probably continue to use it for these inconsequential projects where my own activation energy is the main roadblock.

You can find the repository on SourceHut: ~jakob/srs-anywhere.

Footnotes:

1

For more on the concept, I would highly recommend Michael Nielsen's article Augmenting Long-term Memory (2018) and Nicky Case's How To Remember Anything Forever-ish (2018).

2

Regrettably, I suppose. I'm drawn to Qwen because it's Apache-licensed and actually quite good. I'm hopeful that we'll have more decent open-weight models from friendlier nations in the near future, but for now it's hard to find something comparable to DeepSeek or Qwen.

3

They aren't sanitized because I can't think of a reason where you would care about XSS or other forms of content injection in a single-page application that you've loaded from the local filesystem – other than vulnerabilities that would be exploitable from any untrusted website. If you can come up with a way to exploit this in a legitimately useful way, please write me and let me know.

Comments for this page

    Click here to write a comment on this post.