navigate('/')}>Home
diff --git a/src/experiments/rendering/reflow-repaint/index.jsx b/src/experiments/rendering/reflow-repaint/index.jsx
new file mode 100644
index 0000000..50010fa
--- /dev/null
+++ b/src/experiments/rendering/reflow-repaint/index.jsx
@@ -0,0 +1,206 @@
+import React, { useState, useEffect, useRef, useTransition } from 'react';
+import Stats from 'stats.js';
+import { Play, Pause, RefreshCw, Zap, Cpu, Loader2 } from 'lucide-react';
+
+const ITEM_COUNT = 3000;
+
+// React.memoλ‘ μ΅μ νλ μμ΄ν
λ μ΄μ΄
+const TestItemLayer = React.memo(({ isOptimized, count }) => {
+ return (
+ <>
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+ >
+ );
+}, (prevProps, nextProps) => {
+ return prevProps.isOptimized === nextProps.isOptimized;
+});
+
+const ReflowRepaint = () => {
+ const [isRunning, setIsRunning] = useState(false);
+ const [isOptimized, setIsOptimized] = useState(false);
+
+ // β
[ν΅μ¬ λ³κ²½] React 18μ λμμ± λͺ¨λ ν
μ¬μ©
+ // isPending: μμ
μ΄ μ§ν μ€μΌ λ μλμΌλ‘ trueκ° λ¨
+ // startTransition: λ¬΄κ±°μ΄ μν μ
λ°μ΄νΈλ₯Ό λννλ ν¨μ
+ const [isPending, startTransition] = useTransition();
+
+ const containerRef = useRef(null);
+ const requestRef = useRef();
+ const statsRef = useRef(null);
+
+ // Stats μ΄κΈ°ν
+ useEffect(() => {
+ if (!statsRef.current && containerRef.current) {
+ const stats = new Stats();
+ stats.showPanel(0);
+ stats.dom.style.position = 'absolute';
+ stats.dom.style.top = '10px';
+ stats.dom.style.left = '10px';
+ stats.dom.style.zIndex = '20';
+ containerRef.current.appendChild(stats.dom);
+ statsRef.current = stats;
+ }
+ }, []);
+
+ // β
[ν΅μ¬ λ³κ²½] setTimeout μ κ±° λ° startTransition μ μ©
+ const handleModeToggle = () => {
+ if (isPending) return;
+
+ setIsRunning(false); // μ λλ©μ΄μ
λ©μΆ€ (μ¦μ λ°μ)
+
+ // λ¬΄κ±°μ΄ λ λλ§(3000κ° μ
λ°μ΄νΈ)μ νΈλμ§μ
μΌλ‘ κ°μΈμ λ°±κ·ΈλΌμ΄λ μ²λ¦¬
+ startTransition(() => {
+ setIsOptimized((prev) => !prev);
+ });
+ };
+
+ const animate = (time) => {
+ if (statsRef.current) statsRef.current.begin();
+ const items = document.getElementsByClassName('test-item');
+ const position = (Math.sin(time / 500) + 1) * 150;
+
+ for (let i = 0, len = items.length; i < len; i++) {
+ const item = items[i];
+ if (isOptimized) {
+ item.style.transform = `translate3d(${position}px, 0, 0)`;
+ } else {
+ item.style.left = `${position}px`;
+ }
+ }
+
+ if (statsRef.current) statsRef.current.end();
+ requestRef.current = requestAnimationFrame(animate);
+ };
+
+ useEffect(() => {
+ if (isRunning && !isPending) {
+ requestRef.current = requestAnimationFrame(animate);
+ } else {
+ cancelAnimationFrame(requestRef.current);
+ }
+ return () => cancelAnimationFrame(requestRef.current);
+ }, [isRunning, isOptimized, isPending]);
+
+ return (
+
+
+
+
+
+ Reflow vs Repaint
+
+ Day 1
+
+
+
+ CSS μμ±μ λ°λ₯Έ λ λλ§ νμ΄νλΌμΈ λΆν μ°¨μ΄λ₯Ό λΉκ΅ν©λλ€.
+
+
+
+
+
+
+
+
+
+
+
+
+ {isOptimized ? : }
+
+
+
+ {isOptimized ? 'GPU Accelerated' : 'CPU Software Mode'}
+
+
+ {isOptimized ? 'Composite Layer Only' : 'Triggers Reflow & Layout'}
+
+
+
+
+
+
+
+
+ π‘ μ΄λ‘ : {isOptimized
+ ? 'transform μμ±μ λ©μΈ μ€λ λμ λ μ΄μμ κ³μ°(Reflow)μ 건λλ°κ³ , GPUκ° μ²λ¦¬νλ ν©μ±(Composite) λ¨κ³λ§ μνν©λλ€.'
+ : 'left/top μμ±μ λ³κ²½νλ©΄ λΈλΌμ°μ κ° λͺ¨λ ν½μ
μ μμΉλ₯Ό μ¬κ³μ°(Reflow)νλλΌ CPU μμμ μ¬νκ² μλͺ¨ν©λλ€.'}
+
+
+
+
+ {/* β
λ‘λ© μ€λ²λ μ΄ μ‘°κ±΄ λ³κ²½ */}
+ {isPending && (
+
+
+
Optimizing Rendering...
+
Processing {ITEM_COUNT} Items
+
+ )}
+
+
+ Object Count: {ITEM_COUNT}
+
+
+
+
+
+ );
+};
+
+export default ReflowRepaint;
\ No newline at end of file