From cd41a996c1cad95ae926134634c89ac57e98f660 Mon Sep 17 00:00:00 2001 From: Yoon seokchan Date: Tue, 13 Jan 2026 02:31:13 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Reflow=20vs=20Repaint=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EC=84=B1=EB=8A=A5=20=EB=B9=84=EA=B5=90=20?= =?UTF-8?q?=EC=8B=A4=ED=97=98=20=EA=B5=AC=ED=98=84=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 + src/components/Layout.jsx | 3 +- .../rendering/reflow-repaint/index.jsx | 202 ++++++++++++++++++ 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/experiments/rendering/reflow-repaint/index.jsx 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..5f1571a --- /dev/null +++ b/src/experiments/rendering/reflow-repaint/index.jsx @@ -0,0 +1,202 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Stats from 'stats.js'; +import { Play, Pause, RefreshCw, AlertTriangle, CheckCircle, Cpu, Zap, Loader2 } from 'lucide-react'; + +const ITEM_COUNT = 3000; + +const ReflowRepaint = () => { + const [isRunning, setIsRunning] = useState(false); + const [isOptimized, setIsOptimized] = useState(false); // false: CPU, true: GPU + const [isSwitching, setIsSwitching] = useState(false); // μ „ν™˜ μ• λ‹ˆλ©”μ΄μ…˜ μƒνƒœ + + const containerRef = useRef(null); + const requestRef = useRef(); + const statsRef = useRef(null); + + // 1. Stats.js μ΄ˆκΈ°ν™” + useEffect(() => { + if (!statsRef.current && containerRef.current) { + const stats = new Stats(); + stats.showPanel(0); // FPS + 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; + } + }, []); + + // 2. μ—”μ§„ λͺ¨λ“œ λ³€κ²½ ν•Έλ“€λŸ¬ (μ „ν™˜ 효과 μΆ”κ°€) + const handleModeToggle = () => { + if (isSwitching) return; + + setIsSwitching(true); + setIsRunning(false); + setIsOptimized((prev) => !prev); + setIsSwitching(false); + }; + + // 3. μ• λ‹ˆλ©”μ΄μ…˜ 루프 + const animate = (time) => { + if (statsRef.current) statsRef.current.begin(); + + const items = document.getElementsByClassName('test-item'); + // 0 ~ 300px 사이 왕볡 μš΄λ™ + const position = (Math.sin(time / 500) + 1) * 150; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (isOptimized) { + // βœ… GPU: transform + item.style.transform = `translateX(${position}px)`; + } else { + // ❌ CPU: left + item.style.left = `${position}px`; + } + } + + if (statsRef.current) statsRef.current.end(); + requestRef.current = requestAnimationFrame(animate); + }; + + // μ‹€ν–‰/쀑지 컨트둀 + useEffect(() => { + if (isRunning && !isSwitching) { + requestRef.current = requestAnimationFrame(animate); + } else { + cancelAnimationFrame(requestRef.current); + } + return () => cancelAnimationFrame(requestRef.current); + }, [isRunning, isOptimized, isSwitching]); + + return ( +
+ {/* πŸŽ›οΈ Header & Controls */} +
+
+
+

+ Reflow vs Repaint + + Day 1 + +

+

+ CSS 속성에 λ”°λ₯Έ λ Œλ”λ§ νŒŒμ΄ν”„λΌμΈ λΆ€ν•˜ 차이λ₯Ό λΉ„κ΅ν•©λ‹ˆλ‹€. +

+
+ +
+ {/* Play/Pause Button */} + + + +
+
+ + {/* πŸ•ΉοΈ Toggle Switch UI (New) */} +
+
+
+ {isOptimized ? : } +
+
+
+ {isOptimized ? 'GPU Accelerated' : 'CPU Software Mode'} +
+
+ {isOptimized ? 'Composite Layer Only' : 'Triggers Reflow & Layout'} +
+
+
+ + {/* Custom Toggle Switch */} + +
+ + {/* Info Box */} +
+ πŸ’‘ 이둠: {isOptimized + ? 'transform 속성은 메인 μŠ€λ ˆλ“œμ˜ λ ˆμ΄μ•„μ›ƒ 계산(Reflow)을 κ±΄λ„ˆλ›°κ³ , GPUκ°€ μ²˜λ¦¬ν•˜λŠ” ν•©μ„±(Composite) λ‹¨κ³„λ§Œ μˆ˜ν–‰ν•©λ‹ˆλ‹€.' + : 'left/top 속성을 λ³€κ²½ν•˜λ©΄ λΈŒλΌμš°μ €κ°€ λͺ¨λ“  ν”½μ…€μ˜ μœ„μΉ˜λ₯Ό μž¬κ³„μ‚°(Reflow)ν•˜λŠλΌ CPU μžμ›μ„ μ‹¬ν•˜κ²Œ μ†Œλͺ¨ν•©λ‹ˆλ‹€.'} +
+
+ + {/* Animation Stage */} +
+ {/* Loading Overlay */} + {isSwitching && ( +
+ +
Switching Rendering Engine...
+
Flush Layout Cache & Optimizing Layers
+
+ )} + +
+ Object Count: {ITEM_COUNT} +
+ + {/* Test Items */} + {Array.from({ length: ITEM_COUNT }).map((_, i) => ( +
+ ))} +
+
+ ); +}; + +export default ReflowRepaint; \ No newline at end of file From ec69c573547a4b0dfab92d3848c091ffa2ef8a25 Mon Sep 17 00:00:00 2001 From: Yoon seokchan Date: Tue, 13 Jan 2026 03:02:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?perf:=20React.memo=20=EB=B0=8F=20useTransit?= =?UTF-8?q?ion=20=EB=8F=84=EC=9E=85=ED=95=98=EC=97=AC=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EB=B3=91=EB=AA=A9=20=ED=95=B4=EA=B2=B0=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rendering/reflow-repaint/index.jsx | 120 +++++++++--------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/src/experiments/rendering/reflow-repaint/index.jsx b/src/experiments/rendering/reflow-repaint/index.jsx index 5f1571a..50010fa 100644 --- a/src/experiments/rendering/reflow-repaint/index.jsx +++ b/src/experiments/rendering/reflow-repaint/index.jsx @@ -1,23 +1,51 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useTransition } from 'react'; import Stats from 'stats.js'; -import { Play, Pause, RefreshCw, AlertTriangle, CheckCircle, Cpu, Zap, Loader2 } from 'lucide-react'; +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); // false: CPU, true: GPU - const [isSwitching, setIsSwitching] = 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); - // 1. Stats.js μ΄ˆκΈ°ν™” + // Stats μ΄ˆκΈ°ν™” useEffect(() => { if (!statsRef.current && containerRef.current) { const stats = new Stats(); - stats.showPanel(0); // FPS + stats.showPanel(0); stats.dom.style.position = 'absolute'; stats.dom.style.top = '10px'; stats.dom.style.left = '10px'; @@ -27,31 +55,28 @@ const ReflowRepaint = () => { } }, []); - // 2. μ—”μ§„ λͺ¨λ“œ λ³€κ²½ ν•Έλ“€λŸ¬ (μ „ν™˜ 효과 μΆ”κ°€) + // βœ… [핡심 λ³€κ²½] setTimeout 제거 및 startTransition 적용 const handleModeToggle = () => { - if (isSwitching) return; + if (isPending) return; - setIsSwitching(true); - setIsRunning(false); - setIsOptimized((prev) => !prev); - setIsSwitching(false); + setIsRunning(false); // μ• λ‹ˆλ©”μ΄μ…˜ 멈좀 (μ¦‰μ‹œ 반영) + + // 무거운 λ Œλ”λ§(3000개 μ—…λ°μ΄νŠΈ)을 νŠΈλžœμ§€μ…˜μœΌλ‘œ κ°μ‹Έμ„œ λ°±κ·ΈλΌμš΄λ“œ 처리 + startTransition(() => { + setIsOptimized((prev) => !prev); + }); }; - // 3. μ• λ‹ˆλ©”μ΄μ…˜ 루프 const animate = (time) => { if (statsRef.current) statsRef.current.begin(); - const items = document.getElementsByClassName('test-item'); - // 0 ~ 300px 사이 왕볡 μš΄λ™ const position = (Math.sin(time / 500) + 1) * 150; - for (let i = 0; i < items.length; i++) { + for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; if (isOptimized) { - // βœ… GPU: transform - item.style.transform = `translateX(${position}px)`; + item.style.transform = `translate3d(${position}px, 0, 0)`; } else { - // ❌ CPU: left item.style.left = `${position}px`; } } @@ -60,27 +85,25 @@ const ReflowRepaint = () => { requestRef.current = requestAnimationFrame(animate); }; - // μ‹€ν–‰/쀑지 컨트둀 useEffect(() => { - if (isRunning && !isSwitching) { + if (isRunning && !isPending) { requestRef.current = requestAnimationFrame(animate); } else { cancelAnimationFrame(requestRef.current); } return () => cancelAnimationFrame(requestRef.current); - }, [isRunning, isOptimized, isSwitching]); + }, [isRunning, isOptimized, isPending]); return (
- {/* πŸŽ›οΈ Header & Controls */}

Reflow vs Repaint - Day 1 - + Day 1 +

CSS 속성에 λ”°λ₯Έ λ Œλ”λ§ νŒŒμ΄ν”„λΌμΈ λΆ€ν•˜ 차이λ₯Ό λΉ„κ΅ν•©λ‹ˆλ‹€. @@ -88,10 +111,9 @@ const ReflowRepaint = () => {

- {/* Play/Pause Button */} -
- {/* πŸ•ΉοΈ Toggle Switch UI (New) */}
@@ -128,33 +147,32 @@ const ReflowRepaint = () => {
- {/* Custom Toggle Switch */}
- {/* Info Box */}
πŸ’‘ 이둠: {isOptimized ? 'transform 속성은 메인 μŠ€λ ˆλ“œμ˜ λ ˆμ΄μ•„μ›ƒ 계산(Reflow)을 κ±΄λ„ˆλ›°κ³ , GPUκ°€ μ²˜λ¦¬ν•˜λŠ” ν•©μ„±(Composite) λ‹¨κ³„λ§Œ μˆ˜ν–‰ν•©λ‹ˆλ‹€.' @@ -162,17 +180,16 @@ const ReflowRepaint = () => {
- {/* Animation Stage */}
- {/* Loading Overlay */} - {isSwitching && ( + {/* βœ… λ‘œλ”© μ˜€λ²„λ ˆμ΄ 쑰건 λ³€κ²½ */} + {isPending && (
-
Switching Rendering Engine...
-
Flush Layout Cache & Optimizing Layers
+
Optimizing Rendering...
+
Processing {ITEM_COUNT} Items
)} @@ -180,20 +197,7 @@ const ReflowRepaint = () => { Object Count: {ITEM_COUNT}
- {/* Test Items */} - {Array.from({ length: ITEM_COUNT }).map((_, i) => ( -
- ))} +
);