diff --git a/src/App.jsx b/src/App.jsx index 7ec28b4..0ce89a4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import Layout from './components/Layout'; import Home from './pages/Home'; +import ReflowRepaint from './experiments/rendering/reflow-repaint/index'; function App() { return ( @@ -9,6 +10,7 @@ function App() { }> } /> + } /> } /> diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index e3768a6..5ea567f 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -115,8 +115,7 @@ const Layout = () => { {/* πŸ–₯️ Main Content Area */} -
- {/* Top Header */} +
{/* Top Header */}
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