렌더링 성능 최적화 방법
중요도: ⭐⭐⭐⭐⭐
사용자 경험을 크게 좌우하는 핵심 성능 최적화 기술입니다.
렌더링 성능 최적화는 사용자에게 더 빠르고 부드러운 경험을 제공하기 위한 핵심 기술입니다.
주요 최적화 전략
1. Critical Rendering Path 최적화
첫 화면 렌더링에 필요한 리소스를 우선적으로 로드합니다.
<!-- Above-the-fold CSS를 인라인으로 -->
<style>
.hero-section {
height: 100vh;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
display: flex;
align-items: center;
justify-content: center;
}
.hero-title {
font-size: 3rem;
color: white;
text-align: center;
}
</style>
<!-- 비중요한 CSS는 비동기 로드 -->
<link
rel="preload"
href="styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="styles.css" /></noscript>
<!-- 중요한 폰트 미리 로드 -->
<link
rel="preload"
href="fonts/main-font.woff2"
as="font"
type="font/woff2"
crossorigin
/>2. 이미지 최적화
<!-- 반응형 이미지 -->
<picture>
<source media="(max-width: 768px)" srcset="mobile.webp" type="image/webp" />
<source media="(max-width: 768px)" srcset="mobile.jpg" />
<source srcset="desktop.webp" type="image/webp" />
<img src="desktop.jpg" alt="Responsive image" loading="lazy" />
</picture>
<!-- Intersection Observer를 활용한 Lazy Loading -->
<img
src="placeholder.jpg"
data-src="actual-image.jpg"
class="lazy-image"
alt="Lazy loaded image"
/>JavaScript를 통한 이미지 최적화
// Intersection Observer로 이미지 지연 로딩
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
// 실제 이미지 로드
const actualSrc = img.dataset.src;
if (actualSrc) {
img.src = actualSrc;
img.classList.remove("lazy-image");
img.classList.add("loaded");
}
imageObserver.unobserve(img);
}
});
});
// lazy 클래스를 가진 모든 이미지 관찰
document.querySelectorAll(".lazy-image").forEach((img) => {
imageObserver.observe(img);
});
// 이미지 압축 및 포맷 최적화
function getOptimalImageSrc(baseSrc, width) {
// 디바이스 픽셀 비율 고려
const dpr = window.devicePixelRatio || 1;
const actualWidth = Math.ceil(width * dpr);
// WebP 지원 확인
const supportsWebP = (function () {
const canvas = document.createElement("canvas");
return canvas.toDataURL("image/webp").indexOf("data:image/webp") === 0;
})();
const extension = supportsWebP ? ".webp" : ".jpg";
return `${baseSrc}-${actualWidth}w${extension}`;
}3. JavaScript 최적화
// 디바운싱으로 이벤트 최적화
function debounce(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func(...args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func(...args);
};
}
// 스로틀링으로 연속 이벤트 제어
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// 스크롤 이벤트 최적화
const optimizedScrollHandler = throttle(() => {
const scrollY = window.scrollY;
// 스크롤 위치에 따른 헤더 스타일 변경
if (scrollY > 100) {
document.body.classList.add("scrolled");
} else {
document.body.classList.remove("scrolled");
}
}, 16); // 60fps에 맞춰 16ms
window.addEventListener("scroll", optimizedScrollHandler);
// Intersection Observer로 효율적인 요소 감지
const contentObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 화면에 보일 때만 애니메이션 실행
entry.target.classList.add("animate");
// 필요한 데이터 로드
loadContentData(entry.target);
// 관찰 중단 (한 번만 실행)
contentObserver.unobserve(entry.target);
}
});
},
{
rootMargin: "50px 0px", // 50px 미리 트리거
threshold: 0.1,
},
);
document.querySelectorAll(".lazy-content").forEach((el) => {
contentObserver.observe(el);
});4. DOM 조작 최적화
잦은 DOM 조작은 리플로우와 리페인트를 발생시켜 성능을 저하시킵니다.
// ❌ 비효율적인 DOM 조작
function inefficientUpdate(items) {
const container = document.getElementById("container");
// 매번 DOM에 직접 추가 (매우 느림)
items.forEach((item) => {
const element = document.createElement("div");
element.textContent = item.title;
element.className = "item";
container.appendChild(element); // 리플로우 발생
});
}
// ✅ 효율적인 DOM 조작
function efficientUpdate(items) {
const container = document.getElementById("container");
// DocumentFragment 사용
const fragment = document.createDocumentFragment();
items.forEach((item) => {
const element = document.createElement("div");
element.textContent = item.title;
element.className = "item";
fragment.appendChild(element); // 메모리에서만 작업
});
// 한 번에 DOM에 추가 (리플로우 1번만 발생)
container.appendChild(fragment);
}
// Virtual Scrolling 구현
class VirtualScrollList {
constructor(container, items, itemHeight = 50, visibleCount = 10) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = visibleCount;
this.startIndex = 0;
this.init();
}
init() {
// 전체 높이 설정
this.container.style.height = `${this.items.length * this.itemHeight}px`;
this.container.style.position = "relative";
this.render();
// 스크롤 이벤트 처리
this.container.addEventListener(
"scroll",
throttle(() => this.handleScroll(), 16),
);
}
handleScroll() {
const scrollTop = this.container.scrollTop;
const newStartIndex = Math.floor(scrollTop / this.itemHeight);
if (newStartIndex !== this.startIndex) {
this.startIndex = newStartIndex;
this.render();
}
}
render() {
// 기존 요소 제거
this.container.innerHTML = "";
const endIndex = Math.min(
this.startIndex + this.visibleCount,
this.items.length,
);
for (let i = this.startIndex; i < endIndex; i++) {
const item = this.items[i];
const element = document.createElement("div");
element.style.position = "absolute";
element.style.top = `${i * this.itemHeight}px`;
element.style.height = `${this.itemHeight}px`;
element.textContent = item.title;
this.container.appendChild(element);
}
}
}5. CSS 최적화
/* GPU 가속 활용 */
.animated-element {
/* will-change로 브라우저에게 변경될 속성 알림 */
will-change: transform, opacity;
/* 하드웨어 가속 강제 활성화 */
transform: translateZ(0);
/* 또는 */
transform: translate3d(0, 0, 0);
}
/* 효율적인 애니메이션 */
.smooth-animation {
/* 리플로우를 발생시키는 속성들 피하기 */
/* width: 200px; → transform: scaleX(2); */
/* left: 100px; → transform: translateX(100px); */
/* composite만 발생시키는 속성들 사용 */
transform: translateX(100px) scale(1.2);
opacity: 0.8;
}
/* 복잡한 선택자 피하기 */
/* ❌ 비효율적 */
div > div > div > .item:nth-child(3n + 1) {
color: red;
}
/* ✅ 효율적 */
.specific-item {
color: red;
}
/* containment 속성 활용 */
.independent-section {
/* 이 영역의 스타일/레이아웃 변경이 다른 영역에 영향 없음을 명시 */
contain: layout style paint;
}
/* aspect-ratio로 레이아웃 시프트 방지 */
.video-container {
aspect-ratio: 16/9;
width: 100%;
}
.video-container iframe {
width: 100%;
height: 100%;
}
/* 폰트 로딩 최적화 */
@font-face {
font-family: "CustomFont";
src: url("font.woff2") format("woff2");
font-display: swap; /* 폰트 로딩 중 fallback 폰트 표시 */
}6. requestAnimationFrame 활용
// 부드러운 애니메이션을 위한 프레임 기반 업데이트
class SmoothAnimator {
constructor() {
this.animations = new Set();
this.isRunning = false;
}
add(animation) {
this.animations.add(animation);
this.start();
}
remove(animation) {
this.animations.delete(animation);
if (this.animations.size === 0) {
this.stop();
}
}
start() {
if (!this.isRunning) {
this.isRunning = true;
this.tick();
}
}
stop() {
this.isRunning = false;
}
tick() {
if (!this.isRunning) return;
const now = performance.now();
this.animations.forEach((animation) => {
if (animation.update) {
const finished = animation.update(now);
if (finished) {
this.animations.delete(animation);
}
}
});
if (this.animations.size > 0) {
requestAnimationFrame(() => this.tick());
} else {
this.isRunning = false;
}
}
}
// 사용 예시
const animator = new SmoothAnimator();
function createFadeAnimation(element, duration = 1000) {
const startTime = performance.now();
const startOpacity = parseFloat(getComputedStyle(element).opacity);
return {
update(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
// easing 함수 적용
const easedProgress = easeOutCubic(progress);
element.style.opacity = startOpacity + (1 - startOpacity) * easedProgress;
return progress >= 1;
},
};
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// 애니메이션 실행
const fadeAnimation = createFadeAnimation(element);
animator.add(fadeAnimation);7. 성능 측정 및 모니터링
// Performance API를 활용한 성능 측정
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.initObservers();
}
initObservers() {
// Paint 이벤트 관찰
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name === "first-contentful-paint") {
this.metrics.fcp = entry.startTime;
console.log(`FCP: ${entry.startTime}ms`);
}
});
}).observe({ entryTypes: ["paint"] });
// Largest Contentful Paint 관찰
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
console.log(`LCP: ${lastEntry.startTime}ms`);
}).observe({ entryTypes: ["largest-contentful-paint"] });
// Layout Shift 관찰
new PerformanceObserver((list) => {
let clsScore = 0;
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
});
this.metrics.cls = clsScore;
console.log(`CLS: ${clsScore}`);
}).observe({ entryTypes: ["layout-shift"] });
}
// 커스텀 메트릭 측정
measureCustom(name, fn) {
const start = performance.now();
const result = fn();
const end = performance.now();
this.metrics[name] = end - start;
console.log(`${name}: ${end - start}ms`);
return result;
}
// 메트릭을 서버로 전송
sendMetrics() {
fetch("/analytics/performance", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...this.metrics,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
}),
});
}
}
// 사용
const monitor = new PerformanceMonitor();
// 페이지 언로드 시 메트릭 전송
window.addEventListener("beforeunload", () => {
monitor.sendMetrics();
});성능 지표 (Core Web Vitals)
주요 지표
- FCP (First Contentful Paint): 첫 번째 콘텐츠 렌더링 시간 (< 1.8초)
- LCP (Largest Contentful Paint): 가장 큰 콘텐츠 렌더링 시간 (< 2.5초)
- FID (First Input Delay): 첫 번째 입력 지연 시간 (< 100ms)
- CLS (Cumulative Layout Shift): 누적 레이아웃 이동 점수 (< 0.1)
측정 도구
- Chrome DevTools Performance 탭
- Lighthouse (내장 또는 CLI)
- WebPageTest (외부 도구)
- Core Web Vitals 확장 프로그램
최적화 우선순위
모든 최적화를 한 번에 적용하기보다는 실제 사용자 데이터를 기반으로 병목지점을 파악하고 우선순위를 정하여 단계적으로 적용하는 것이 효과적입니다.