I have a talk that teaches students how to set the most beautiful seminar paper. It is written in LaTeX, obviously, because a LaTeX talk made in PowerPoint would be the academic equivalent of a vegan butcher. It has kerning diagrams. It has glyph boxes. It has a slide that explains, with a straight face, that LaTeX is worth the effort.

It also took over 90 minutes to compile.

The workaround I called a feature

Beamer has a handout mode that collapses all the animations into single slides. The handout version compiled in a perfectly normal amount of time. The full version, the one with the overlays that reveal bullet points one by one like a very slow magician, took an hour and a half, and more if you were impatient enough to check.

So I compiled the handout and moved on. Every time I touched a slide, I told myself the animations were “optional” and that “nobody needs to see the bullet points appear.” This is the same energy as buying a sports car and only ever driving it in first gear because second gear makes a noise.

Measuring instead of guessing

Staring at a 1400-line .tex file and guessing is a great way to spend an afternoon and learn nothing. So instead I hooked into every page shipout and had Lua print a timestamp:

\AddToHook{shipout/after}{\directlua{profpage()}}

My first attempt at this put the Lua string.format("%.2f", ...) inline. In TeX, % starts a comment. The compile died instantly, which, to be fair, was the fastest it had ever finished.

With the Lua moved into its own file, the numbers came out:

  • Pages 1 to 76: roughly 0.3 seconds each. Fine. Healthy. Nothing to see here.
  • Page 91: 1.9 seconds.
  • Page 95: 3 seconds.
  • Page 100: 5 seconds, and climbing.

Not one slow slide. Every slide slower than the last. That is not a heavy frame. That is a quadratic.

The culprit

The talk uses smile, my own little LaTeX package, which adds a listings option to append extra literate replacements to a language. Here it is, in its full glory, including the original comment:

% Holy shit, this took some trial and error. Expansion is like magic some times
\lst@Key{apptoliterate}{}{\xdef\lst@literate{\unexpanded\expandafter{\lst@literate}\unexpanded{#1}}}

\xdef. The x stands for “expanded.” The g hidden inside it stands for global.

Every time a listing selected my LaTeX highlighting language, it appended its dozen literate rules to a global list. That list never got reset. The next listing got the old list plus another dozen. The one after that, another dozen on top. And every single listing had to chew through the entire accumulated pile to set itself up.

In handout mode that is annoying. In the full version, Beamer typesets a frame once per overlay, so a slide that reveals thirteen commands one after another runs its inline listings over and over again, each round feeding the pile for the next. The talk was, quite literally, getting worse at compiling the longer it compiled.

Past me left a comment saying expansion “is like magic.” It was. Specifically the kind where the apprentice enchants a broom to fetch water and forgets how to make it stop.

The fix

-\lst@Key{apptoliterate}{}{\xdef\lst@literate{...}}
+\lst@Key{apptoliterate}{}{\edef\lst@literate{...}}

One letter. listings already applies its keys inside a group, so a local \edef is exactly what was wanted all along: each listing starts from the preamble state, adds its own rules, and forgets them when it’s done.

A full pass of the complete, animated, 295-page version now takes about 25 seconds. From 90 minutes and more. That’s a speedup of “go make lunch” to “take a sip of water.”

The one slide that changed

To check nothing broke, I rendered the handout before and after and diffed them pixel by pixel. 83 of 84 pages were identical.

The odd one out was the BibTeX example. Its curly braces used to be gray, and now they were black. That listing doesn’t even use my LaTeX language. It had been gray only because it inherited the leaked rules from every LaTeX listing that came before it.

So the bug wasn’t just slow, it was styling my slides, and I had looked at that slide for months thinking “nice, subtle braces, good choice.” It was not my choice. It was pollution. I kept the gray, now deliberately, because it did look nice.

In conclusion

The talk that teaches students why LaTeX is worth it now compiles fast enough that I can actually show them the animations. The fix was one character. Finding it required admitting that “the handout is fine” was a coping mechanism, not a design decision.

If your LaTeX document gets slower the further it gets, don’t look for the heavy slide. Look for something global that only ever grows. And maybe be suspicious of any line whose comment calls it magic.