I am experimenting with the end-to-end latency of Google Docs: how long it takes for one user's edits to show up for other users. To measure this, I need a way to detect when the text in a Google Doc changes. Ideally, each change would be logged to the console.
This used to be possible by watching the raw HTML, but Google Docs switched to using their own Canvas in ~2021. I have also tried some 2022-era tricks to switch it to legacy HTML mode (window._docs_force_html_by_ext = true
; installing Grammarly extension) but they no longer seem to work.
Other things I've tried:
Here is what worked for me:
window._docs_annotate_canvas_by_ext = "<extension ID>";
Here <extension ID>
must be the ID of a Chrome extension that is whitelisted to work with Google Docs. You can found a list of valid IDs by searching for i8d=
in the code. (Source)
Now Google Docs will use the same Canvas-based renderer, but it will also add SVG elements to the DOM, whose aria-label
attribute contains the text. I believe it is "drawing" rects to indicate where snippets of text are.
The SVG elements are deep under the docs-editor-container
div, near the actual <canvas>
element.
To detect text changes, use a MutationObserver:
const contentRoot = document.querySelector(".kix-rotatingtilemanager-content");
new MutationObserver(onMutation).observe(
contentRoot,
{subtree: true, childList: true}
);
Empirically, when the text changes, Google Docs will remove an old node and add a new one with the updated aria-label
. Text with different inline formats will be split across multiple nodes; header-formatted text doesn't show up at all; and nodes also appear/disappear as text is scrolled into/out of view. This is my best attempt at processing it:
function onMutation(mutationList) {
for (const mutation of mutationList) {
if (mutation.addedNodes.length === 0) continue;
if (mutation.removedNodes.length === 0) {
// It may have been scrolled into view - not actually new text.
continue;
}
let after = "";
for (const added of mutation.addedNodes) {
after += added.ariaLabel ?? "";
}
let before = "";
for (const removed of mutation.removedNodes) {
before += removed.ariaLabel ?? "";
}
if (after !== before) {
// Text changed from "before" to "after"...
}
}
}