フロントエンド最適化:パフォーマンス向上テクニック
Webアプリケーションのパフォーマンスは、ユーザー体験に直接影響します。この記事では、フロントエンドのパフォーマンスを測定し、最適化する実践的なテクニックを学びます。読み込み時間の短縮、レンダリングの最適化、バンドルサイズの削減など、具体的な手法を実装しながら理解を深めます。
モダンなWeb開発において必須となる最適化スキルを身につけましょう。
1. パフォーマンス測定
1.1 Web Vitals
Web Vitalsは、ユーザー体験を測定するための指標です。
// Web Vitalsの測定
import {getCLS, getFID, getFCP, getLCP, getTTFB} from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
navigator.sendBeacon('/analytics', body);
}
getCLS(sendToAnalytics); // Cumulative Layout Shift
getFID(sendToAnalytics); // First Input Delay
getFCP(sendToAnalytics); // First Contentful Paint
getLCP(sendToAnalytics); // Largest Contentful Paint
getTTFB(sendToAnalytics); // Time to First Byte
1.2 Lighthouse
# Lighthouse CLIのインストール
npm install -g lighthouse
# Lighthouseで測定
lighthouse https://example.com --view
# カスタム設定
lighthouse https://example.com \
--only-categories=performance \
--chrome-flags="--headless" \
--output=html \
--output-path=./report.html
1.3 Performance API
// Performance APIを使った測定
const perfData = window.performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
console.log('Page load time:', pageLoadTime);
// リソースの読み込み時間
const resources = window.performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log(`${resource.name}: ${resource.duration}ms`);
});
// カスタムメジャー
performance.mark('start');
// 何らかの処理
performance.mark('end');
performance.measure('my-measure', 'start', 'end');
const measure = performance.getEntriesByName('my-measure')[0];
console.log('Duration:', measure.duration);
2. バンドルサイズの最適化
2.1 コード分割
// Reactでのコード分割
import { lazy, Suspense } from 'react';
// 動的インポート
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// ルートベースのコード分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
2.2 ツリーシェイキング
// ❌ 悪い例(全体をインポート)
import _ from 'lodash';
const result = _.debounce(() => {}, 300);
// ✅ 良い例(必要なものだけインポート)
import debounce from 'lodash/debounce';
const result = debounce(() => {}, 300);
// ES6モジュールを使用
// package.jsonで "sideEffects": false を設定
2.3 Webpackの最適化
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
// ミニファイ
mode: 'production',
};
2.4 バンドルアナライザー
# webpack-bundle-analyzerのインストール
npm install --save-dev webpack-bundle-analyzer
# 分析
npx webpack-bundle-analyzer dist/stats.json
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
}),
],
};
3. 画像の最適化
3.1 画像フォーマット
<!-- WebPフォーマットの使用 -->
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>
<!-- AVIFフォーマット(より高効率) -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>
3.2 レスポンシブ画像
<!-- srcsetとsizes属性 -->
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
alt="Description"
loading="lazy"
>
<!-- アートディレクション -->
<picture>
<source media="(max-width: 600px)" srcset="mobile-image.jpg">
<source media="(min-width: 601px)" srcset="desktop-image.jpg">
<img src="desktop-image.jpg" alt="Description">
</picture>
3.3 遅延読み込み
<!-- ネイティブのlazy loading -->
<img src="image.jpg" alt="Description" loading="lazy">
<!-- Intersection Observer API -->
<script>
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
</script>
4. CSSの最適化
4.1 Critical CSS
<!-- Critical CSSをインライン化 -->
<style>
/* Critical CSS - フォールド内のスタイル */
.header { height: 60px; }
.hero { background: #333; }
</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>
4.2 CSSの最小化
// PostCSS + cssnano
// postcss.config.js
module.exports = {
plugins: [
require('cssnano')({
preset: ['default', {
discardComments: {
removeAll: true,
},
}],
}),
],
};
4.3 未使用CSSの削除
// PurgeCSS
// purgecss.config.js
module.exports = {
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
css: ['./src/styles.css'],
};
5. JavaScriptの最適化
5.1 デバウンスとスロットル
// デバウンス
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 使用例
const handleSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
// スロットル
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 使用例
const handleScroll = throttle(() => {
console.log('Scrolling...');
}, 100);
5.2 メモ化
// React.memo
const ExpensiveComponent = React.memo(({ data }) => {
// 重い計算
const processedData = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
return <div>{processedData}</div>;
});
// useMemo
const memoizedValue = useMemo(() => {
return expensiveFunction(a, b);
}, [a, b]);
// useCallback
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
5.3 仮想スクロール
// react-windowの使用
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
);
}
6. リソースの読み込み最適化
6.1 リソースヒント
<!-- DNSプリフェッチ -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!-- プリコネクト -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- プリロード -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="hero-image.jpg" as="image">
<!-- プリフェッチ -->
<link rel="prefetch" href="/next-page.html">
<!-- プリレンダー -->
<link rel="prerender" href="https://example.com/next-page">
6.2 リソースの優先度
<!-- 高い優先度 -->
<link rel="stylesheet" href="critical.css">
<script src="critical.js"></script>
<!-- 低い優先度 -->
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
<script src="analytics.js" defer></script>
<!-- async属性 -->
<script src="analytics.js" async></script>
<!-- defer属性 -->
<script src="app.js" defer></script>
7. キャッシュ戦略
7.1 HTTPキャッシュ
// Service Workerでのキャッシュ
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
caches.open('images-v1').then((cache) => {
return cache.match(event.request).then((response) => {
return response || fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
}
});
// Cache-Controlヘッダ
// 静的アセット: Cache-Control: public, max-age=31536000, immutable
// HTML: Cache-Control: no-cache
7.2 ブラウザキャッシュ
// localStorage
localStorage.setItem('data', JSON.stringify(data));
const data = JSON.parse(localStorage.getItem('data'));
// sessionStorage
sessionStorage.setItem('tempData', JSON.stringify(data));
// IndexedDB(大量データ)
const request = indexedDB.open('myDB', 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(['store'], 'readwrite');
const store = transaction.objectStore('store');
store.put(data, 'key');
};
8. レンダリングの最適化
8.1 仮想DOMの最適化
// Reactの最適化
// 1. key属性の適切な使用
{items.map(item => (
<Item key={item.id} data={item} />
))}
// 2. コンポーネントの分割
const LargeComponent = () => {
return (
<>
<Header />
<MainContent />
<Footer />
</>
);
};
// 3. shouldComponentUpdate / React.memo
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{data}</div>;
}, (prevProps, nextProps) => {
return prevProps.data.id === nextProps.data.id;
});
8.2 レンダリングの遅延
// 遅延レンダリング
function LazyComponent() {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShouldRender(true);
}, 100);
return () => clearTimeout(timer);
}, []);
if (!shouldRender) return null;
return <ExpensiveComponent />;
}
// Intersection Observerでの遅延レンダリング
function LazyRender({ children }) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
});
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, []);
return <div ref={ref}>{isVisible && children}</div>;
}
9. ネットワーク最適化
9.1 HTTP/2 Server Push
// Node.js + Express
app.get('/', (req, res) => {
if (req.httpVersionMajor === 2) {
res.push('/styles.css', {
'content-type': 'text/css'
}).end(fs.readFileSync('./styles.css'));
}
res.send(html);
});
9.2 CDNの活用
<!-- CDNからライブラリを読み込み -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<!-- Subresource Integrity (SRI) -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>
9.3 圧縮
// Gzip/Brotli圧縮
// Expressでの設定
const compression = require('compression');
app.use(compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
10. 実践的な最適化例
10.1 Next.jsアプリケーションの最適化
// next.config.js
module.exports = {
// 画像の最適化
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
},
// 圧縮
compress: true,
// 実験的機能
experimental: {
optimizeCss: true,
},
// Webpackの設定
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.splitChunks.chunks = 'all';
}
return config;
},
};
// 動的インポート
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false,
});
// 画像の最適化
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
placeholder="blur"
/>
11. まとめと次のステップ
この記事を通じて、フロントエンドのパフォーマンス最適化の実践的なテクニックを学びました。
学んだこと
- パフォーマンス測定: Web Vitals、Lighthouse、Performance API
- バンドルサイズの最適化: コード分割、ツリーシェイキング、バンドルアナライザー
- 画像の最適化: フォーマット、レスポンシブ画像、遅延読み込み
- CSSの最適化: Critical CSS、最小化、未使用CSSの削除
- JavaScriptの最適化: デバウンス、メモ化、仮想スクロール
- リソースの読み込み: リソースヒント、優先度制御
- キャッシュ戦略: HTTPキャッシュ、Service Worker
- レンダリングの最適化: 仮想DOM、遅延レンダリング
最適化のベストプラクティス
- 測定を最優先: 最適化の前後で測定
- 段階的な最適化: 大きな影響があるものから
- ユーザー体験を重視: 数値だけでなく体験も考慮
- 継続的な監視: パフォーマンスの継続的な監視
- バランス: 最適化と保守性のバランス
次のステップ
- Core Web Vitalsの改善: LCP、FID、CLSの最適化
- プログレッシブWebアプリ(PWA): Service Worker、オフライン対応
- Edge Computing: Cloudflare Workers、Vercel Edge Functions
- モニタリング: Real User Monitoring (RUM)
- A/Bテスト: 最適化の効果測定
フロントエンドのパフォーマンスは、ユーザー体験とビジネスの成功に直結します。継続的な測定と最適化を通じて、高速で快適なWebアプリケーションを構築しましょう!
Happy Optimizing!
コメント
コメントを読み込み中...
