Lighthouse, Web Vitals, Bundle Analyzer를 활용한 성능 모니터링
성능 데이터 수집 중...
npm install web-vitals
# CLI로 실행
npm install -g lighthouse
lighthouse https://your-site.com --view1. 패키지 설치
npm install -D @next/bundle-analyzer
2. next.config.js 설정
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// your next config
})3. 분석 실행
ANALYZE=true npm run build
// app/layout.tsx
import { Suspense } from 'react';
import { WebVitals } from './web-vitals';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Suspense>
<WebVitals />
</Suspense>
</body>
</html>
);
}
// web-vitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric);
// Analytics로 전송
if (window.gtag) {
window.gtag('event', metric.name, {
value: Math.round(
metric.name === 'CLS' ? metric.value * 1000 : metric.value
),
event_label: metric.id,
non_interaction: true,
});
}
// 커스텀 API로 전송
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
}),
});
});
return null;
}
// 또는 web-vitals 패키지 직접 사용
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
const url = '/api/analytics';
// Navigator.sendBeacon 사용 (페이지 종료 시에도 전송 보장)
if (navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
fetch(url, { body, method: 'POST', keepalive: true });
}
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);// .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
// lighthouserc.js
module.exports = {
ci: {
collect: {
staticDistDir: './out',
url: [
'http://localhost:3000/',
'http://localhost:3000/about',
],
numberOfRuns: 3,
},
assert: {
preset: 'lighthouse:recommended',
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.95 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.95 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};// 커스텀 성능 측정
function measurePerformance() {
// 1. Navigation Timing
const perfData = performance.getEntriesByType('navigation')[0];
console.log('DNS Lookup:', perfData.domainLookupEnd - perfData.domainLookupStart);
console.log('TCP Connection:', perfData.connectEnd - perfData.connectStart);
console.log('Time to First Byte:', perfData.responseStart - perfData.requestStart);
console.log('DOM Interactive:', perfData.domInteractive - perfData.fetchStart);
console.log('DOM Complete:', perfData.domComplete - perfData.fetchStart);
console.log('Load Complete:', perfData.loadEventEnd - perfData.fetchStart);
// 2. Resource Timing
const resources = performance.getEntriesByType('resource');
resources.forEach((resource) => {
console.log(`${resource.name}: ${resource.duration}ms`);
});
// 3. User Timing (커스텀 측정)
performance.mark('api-fetch-start');
fetch('/api/data')
.then(response => response.json())
.then(data => {
performance.mark('api-fetch-end');
performance.measure('api-fetch', 'api-fetch-start', 'api-fetch-end');
const measure = performance.getEntriesByName('api-fetch')[0];
console.log('API Fetch Time:', measure.duration);
});
// 4. Long Tasks 감지
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long Task detected:', entry.duration);
// 50ms 이상 걸리는 작업 추적
if (entry.duration > 50) {
// Analytics로 전송
}
}
});
observer.observe({ entryTypes: ['longtask'] });
// 5. Layout Shift 감지
const layoutShiftObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout Shift:', entry.value);
}
}
});
layoutShiftObserver.observe({ entryTypes: ['layout-shift'] });
}
// React 컴포넌트에서 사용
function MyComponent() {
useEffect(() => {
const startTime = performance.now();
return () => {
const endTime = performance.now();
console.log('Component lifetime:', endTime - startTime);
};
}, []);
return <div>...</div>;
}