perf: sohbet markdown lazy + marked client'tan çıktı, mermaid görünürlükte (Faz 4)

- markdown.tsx: blok bölme marked.lexer yerine çit-farkındalıklı boş satır
  bölücüsü — react-markdown'ın yanında ikinci tam parser (~460 KB paket)
  client'a inmiyordu artık gerekmiyor; memoized blok yapısı aynen korunur
- markdown-lazy: react-markdown+remark zinciri (~215 KB) lazy; fallback
  aynı class'lı düz metin (stream'de flash/CLS yok), sohbet mount'unda
  preloadMarkdown ile chunk önden ısınır — /listem 1315->1133 KB eager JS
- el-cizimi-sema: mermaid importu IntersectionObserver'a bağlandı
  (rootMargin 200px); şemalar hep fold altında, ~1 MB mount'ta inmiyor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 14:01:48 +03:00
parent b7e10efd1d
commit 90ac590737
5 changed files with 110 additions and 27 deletions

View File

@@ -0,0 +1,32 @@
"use client";
// react-markdown + remark zinciri (~215 KB) yalnızca markdown gerçekten
// çizileceği zaman insin. next/dynamic yerine elle sarmalayıcı: `loading`
// children alamıyor, fallback'in AYNI class'lı düz metin olması gerekiyor ki
// stream sırasında iskelet/boşluk flash'ı olmasın (chunk gelince yerinde
// zenginleşir).
import { useEffect, useReducer } from "react";
import type { MarkdownProps } from "./markdown";
let mod: typeof import("./markdown") | null = null;
let soz: Promise<void> | null = null;
/** Chunk'ı önden ısıt (ör. sohbet mount olduğunda). */
export function preloadMarkdown(): Promise<void> {
return (soz ??= import("./markdown").then((m) => {
mod = m;
}));
}
export function MarkdownLazy({
fallback,
...props
}: MarkdownProps & { fallback: React.ReactNode }) {
const [, tazele] = useReducer((c: number) => c + 1, 0);
useEffect(() => {
if (!mod) void preloadMarkdown().then(() => tazele());
}, []);
const Markdown = mod?.Markdown;
return Markdown ? <Markdown {...props} /> : <>{fallback}</>;
}

View File

@@ -1,5 +1,4 @@
import { cn } from "@/lib/utils"
import { marked } from "marked"
import { memo, useId, useMemo } from "react"
import ReactMarkdown, { Components } from "react-markdown"
import remarkBreaks from "remark-breaks"
@@ -13,9 +12,27 @@ export type MarkdownProps = {
components?: Partial<Components>
}
// Stream memoization'ı için bloklara bölme. Eskiden marked.lexer yapıyordu —
// react-markdown'ın micromark'ına ek ikinci bir tam parser'ı (~460 KB paket)
// client'a taşımak blok bölmek için gereksiz: kod çiti dışındaki boş satırlar
// zaten markdown'da blok sınırıdır. Çit içi boş satırlar korunur.
function parseMarkdownIntoBlocks(markdown: string): string[] {
const tokens = marked.lexer(markdown)
return tokens.map((token) => token.raw)
const bloklar: string[] = []
let mevcut: string[] = []
let citte = false
for (const satir of markdown.split("\n")) {
if (/^\s*(```|~~~)/.test(satir)) citte = !citte
if (!citte && satir.trim() === "") {
if (mevcut.length) {
bloklar.push(mevcut.join("\n"))
mevcut = []
}
} else {
mevcut.push(satir)
}
}
if (mevcut.length) bloklar.push(mevcut.join("\n"))
return bloklar.length ? bloklar : [markdown]
}
function extractLanguage(className?: string): string {

View File

@@ -6,7 +6,8 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { Markdown } from "./markdown"
import { MarkdownLazy } from "./markdown-lazy"
import type { MarkdownProps } from "./markdown"
export type MessageProps = {
children: React.ReactNode
@@ -48,7 +49,7 @@ export type MessageContentProps = {
children: React.ReactNode
markdown?: boolean
className?: string
} & React.ComponentProps<typeof Markdown> &
} & MarkdownProps &
React.HTMLProps<HTMLDivElement>
const MessageContent = ({
@@ -63,9 +64,15 @@ const MessageContent = ({
)
return markdown ? (
<Markdown className={classNames} {...props}>
// Fallback aynı class'lı düz metin: chunk inene dek CLS/flash olmadan
// ham içerik görünür, sonra yerinde zenginleşir.
<MarkdownLazy
className={classNames}
fallback={<div className={classNames}>{children}</div>}
{...props}
>
{children as string}
</Markdown>
</MarkdownLazy>
) : (
<div className={classNames} {...props}>
{children}