Split streaming markdown into memoized blocks for mobile perf (#260)

Instead of re-parsing the entire markdown string on every streaming
token, split at paragraph boundaries (double newlines) while keeping
code fences intact. Each block renders as its own React.memo'd
<Markdown> component so only the last active block re-renders.
This commit is contained in:
Mohamed Boudra
2026-04-12 10:23:49 +07:00
committed by GitHub
parent fb5e2608e0
commit f35b0b8b7d
3 changed files with 178 additions and 9 deletions
@@ -0,0 +1,60 @@
function getFenceDelimiter(line: string) {
const match = /^( {0,3})(`{3,}|~{3,})/.exec(line);
return match?.[2] ?? null;
}
export function splitMarkdownBlocks(text: string): string[] {
if (text.length === 0) {
return [];
}
const blocks: string[] = [];
let currentLines: string[] = [];
let activeFenceCharacter: "`" | "~" | null = null;
let activeFenceLength = 0;
let sawBlockSeparator = false;
for (const line of text.split("\n")) {
const isBlankLine = line.trim().length === 0;
if (!activeFenceCharacter && isBlankLine) {
if (currentLines.length > 0) {
sawBlockSeparator = true;
}
continue;
}
if (!activeFenceCharacter && sawBlockSeparator) {
blocks.push(currentLines.join("\n"));
currentLines = [];
sawBlockSeparator = false;
}
currentLines.push(line);
const fenceDelimiter = getFenceDelimiter(line);
if (!fenceDelimiter) {
continue;
}
if (!activeFenceCharacter) {
activeFenceCharacter = fenceDelimiter[0] as "`" | "~";
activeFenceLength = fenceDelimiter.length;
continue;
}
if (
fenceDelimiter[0] === activeFenceCharacter &&
fenceDelimiter.length >= activeFenceLength
) {
activeFenceCharacter = null;
activeFenceLength = 0;
}
}
if (currentLines.length > 0) {
blocks.push(currentLines.join("\n"));
}
return blocks.filter((block) => block.length > 0);
}