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

@@ -14,6 +14,7 @@ import {
ChatContainerScrollAnchor,
} from "@/components/ui/chat-container";
import { Message, MessageContent } from "@/components/ui/message";
import { preloadMarkdown } from "@/components/ui/markdown-lazy";
import {
PromptInput,
PromptInputAction,
@@ -90,6 +91,12 @@ export function SohbetClient({
const [kredi, setKredi] = useState(baslangicKredi);
const router = useRouter();
// Markdown chunk'ını sohbet görünür olur olmaz ısıt: ilk danışman cevabı
// (veya geçmiş mesajlar) çizilirken lazy sınırında düz-metin flash'ı olmasın.
useEffect(() => {
void preloadMarkdown();
}, []);
useEffect(() => {
// Geçmiş sunucudan geldiyse (listem sayfası) ekstra tur atma;
// popup gibi geç mount olan yerlerde eski davranış sürer.

View File

@@ -6,7 +6,7 @@
// Mermaid ağır bir paket olduğu için useEffect içinde dinamik import edilir;
// sayfa paketine girmez.
import { useEffect, useId, useState } from "react";
import { useEffect, useId, useRef, useState } from "react";
import localFont from "next/font/local";
// Şemanın "el çizimi" görünümüne uyan el yazısı fontu (Sweet Cucumber
@@ -30,38 +30,58 @@ export function ElCizimiSema({
figcaption?: React.ReactNode;
}) {
const [svg, setSvg] = useState<string | null>(null);
const figureRef = useRef<HTMLElement>(null);
// mermaid.render'ın istediği DOM id'si: useId'nin ":" karakterleri geçersiz
const semaId = `sema-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
// ~1 MB'lık mermaid mount'ta değil, şema görünürlüğe yaklaşınca insin —
// şemalar hep fold altında (/meraklisina sayfa sonu, rehber makale ortası).
useEffect(() => {
const el = figureRef.current;
if (!el) return;
let iptal = false;
import("mermaid").then(async ({ default: mermaid }) => {
mermaid.initialize({
startOnLoad: false,
look: "handDrawn",
handDrawnSeed: 7, // her render'da aynı çizgi titremesi (deterministik)
theme: "base",
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
themeVariables: {
primaryColor: "#ffffff",
primaryBorderColor: "#e2e8f0",
primaryTextColor: "#0f172a",
lineColor: "#94a3b8",
edgeLabelBackground: "#ffffff",
const io = new IntersectionObserver(
(girisler) => {
if (!girisler.some((g) => g.isIntersecting)) return;
io.disconnect();
cizdir();
},
// Kullanıcı şemaya varmadan yükleme başlasın
{ rootMargin: "200px 0px" },
);
io.observe(el);
function cizdir() {
import("mermaid").then(async ({ default: mermaid }) => {
mermaid.initialize({
startOnLoad: false,
look: "handDrawn",
handDrawnSeed: 7, // her render'da aynı çizgi titremesi (deterministik)
theme: "base",
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
fontSize: "17px", // script fontun x-yüksekliği düşük; okunurluk için büyütüldü
},
themeVariables: {
primaryColor: "#ffffff",
primaryBorderColor: "#e2e8f0",
primaryTextColor: "#0f172a",
lineColor: "#94a3b8",
edgeLabelBackground: "#ffffff",
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
fontSize: "17px", // script fontun x-yüksekliği düşük; okunurluk için büyütüldü
},
});
const sonuc = await mermaid.render(semaId, tanim);
if (!iptal) setSvg(sonuc.svg);
});
const sonuc = await mermaid.render(semaId, tanim);
if (!iptal) setSvg(sonuc.svg);
});
}
return () => {
iptal = true;
io.disconnect();
};
}, [semaId, tanim]);
return (
<figure className="mt-6">
<figure ref={figureRef} className="mt-6">
{svg ? (
<div
role="img"