Rendering Markdown in Quick Look with TextKit instead of WebKit
MD QuickLook is a Quick Look extension that renders Markdown in Finder without WebKit. A Rust core produces a compact layout (text plus blocks, paragraphs and styled runs at UTF-16 offsets), and the Swift side turns it into an NSAttributedString and draws it with TextKit 1:
- quotes, alerts and code blocks are
NSTextBlocks with borders and backgrounds; tables areNSTextTables - images, formulas, Mermaid diagrams, badges and checkboxes are attachment cells
- long documents build the first screen (~60 ms for a 900 KB file) and append the rest in the background
- collapsible
<details>sections work by cutting the folded text out of the storage and re-inserting it on click, since TextKit 1 can't hide paragraphs - math is typeset with SwiftMath, which I extended with KaTeX symbols, aliases and environments
A typical README is ready in ~30 ms. Source (MIT): https://github.com/armamut/md-quicklook Longer write-up: https://medium.com/@armamut/previewing-markdown-on-macos-in-30-ms-a-rust-core-and-textkit-no-web-engine-1adfa2734f48
Curious whether anyone has tried the same with TextKit 2.
1
u/Common_Green_1666 2d ago
Do you find that rendering this way is faster than using WebKit?
My understanding is that WebKit would be faster than textkit 2.
1
u/Ciosciaa 1d ago
That thought is stunning to me. Why do you think WebKit would be faster?
I’m not judging. I have a genuinely limited understanding of the discrepancies between those two engines. I just figured there was a ton more overhead with something like WebKit.
1
u/Common_Green_1666 1d ago
WebKit is optimized to render pages as fast as possible. Apple has strong incentives to make it fast so it can maintain Safari’s market share relative to Chrome, Firefox, etc.
I think your intuition that WebKit would have more overhead is generally correct, but let’s break down what that “overhead” means here.
WebKit does a lot of things besides just rendering HTML. When you use it, you need to use all of it. There’s no easy way to only use the text rendering part. This might mean That using WebKit could lead to higher memory usage than using TextKit. Apps built using Electron are a good example of this. They tend to use lots of memory compared to what an equivalent native app would use.
Here’s a blog post on the topic and a related discussion
2
u/armamut 1d ago
I measured it, since the app has both renderers – you can switch to a WebKit one in settings. For a typical README, request → ready is ~30 ms native vs ~200 ms with WKWebView; for a 900 KB file the first screen is ~60 ms vs ~300 ms.
You're right that WebKit's layout engine itself is very fast. The cost in a Quick Look extension is everything around it: the web content process has to start, the page loads, and the scripts for highlighting, math and diagrams have to run before anything is on screen. TextKit just lays out an attributed string in-process, and since the Rust side has already done the parsing and highlighting, there's very little left to do. Memory is lower too, as you said.
To be fair, this is TextKit 1, not 2 – the tables need NSTextTable, which TextKit 2 doesn't support.
1
u/Financial_app 1d ago
Have you compared the two renderers with VoiceOver? A README with a table, an image with alt text, and a collapsed <details> section would make a useful test alongside the timing numbers. I'm particularly curious whether the attachment cells expose their content, and whether expanding a section leaves VoiceOver's position somewhere predictable.
0
1d ago
1
u/Common_Green_1666 1d ago
Just curious, did you profile where the time was being spent on the WebKit path?
1
u/dimKant 2d ago
The details trick, cutting the folded text out of the storage and putting it back on click, is the kind of thing nobody writes down about TextKit 1. Nice write-up. On TextKit 2: folding gets easier there, because the content manager delegate can just skip elements during enumeration and you never touch the storage. But NSTextTable and NSTextBlock have no TextKit 2 version, and NSTextView quietly falls back to TextKit 1 as soon as it sees one. So with tables and bordered quotes you'd be writing your own NSTextLayoutFragment subclasses for all of it, which is a lot of work for a preview that already opens in 30 ms.