Web Performance Optimization: Panduan Lengkap untuk Website yang Cepat
Performa website adalah faktor krusial yang mempengaruhi user experience, SEO ranking, dan conversion rate. Artikel ini akan membahas teknik-teknik comprehensive untuk mengoptimasi performa website dari berbagai aspek.
Mengapa Performance Penting?
Impact pada Business
- User Experience: 53% pengguna meninggalkan website jika loading time > 3 detik
- SEO Ranking: Google menggunakan Core Web Vitals sebagai ranking factor
- Conversion Rate: Setiap 100ms delay dapat menurunkan conversion hingga 7%
- Revenue: Amazon kehilangan $1.6 miliar per tahun untuk setiap 100ms delay
- Mobile Users: 70% koneksi mobile masih 3G atau lebih lambat
Core Web Vitals
Google menggunakan tiga metrik utama untuk mengukur user experience:
- Largest Contentful Paint (LCP): ≤ 2.5 detik
- First Input Delay (FID): ≤ 100 milidetik
- Cumulative Layout Shift (CLS): ≤ 0.1
Performance Metrics dan Measurement
1. Key Performance Metrics
// Performance measurement dengan Web APIs
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.observer = null;
this.init();
}
init() {
// Measure Core Web Vitals
this.measureLCP();
this.measureFID();
this.measureCLS();
this.measureTTFB();
this.measureFCP();
}
// Largest Contentful Paint
measureLCP() {
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
console.log("LCP:", lastEntry.startTime);
// Send to analytics
this.sendMetric("LCP", lastEntry.startTime);
});
observer.observe({ entryTypes: ["largest-contentful-paint"] });
}
}
// First Input Delay
measureFID() {
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
this.metrics.fid = entry.processingStart - entry.startTime;
console.log("FID:", this.metrics.fid);
this.sendMetric("FID", this.metrics.fid);
});
});
observer.observe({ entryTypes: ["first-input"] });
}
}
// Cumulative Layout Shift
measureCLS() {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries = [];
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
if (
sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
this.metrics.cls = clsValue;
console.log("CLS:", clsValue);
this.sendMetric("CLS", clsValue);
}
}
});
});
observer.observe({ entryTypes: ["layout-shift"] });
}
}
// Time to First Byte
measureTTFB() {
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.entryType === "navigation") {
this.metrics.ttfb = entry.responseStart - entry.requestStart;
console.log("TTFB:", this.metrics.ttfb);
this.sendMetric("TTFB", this.metrics.ttfb);
}
});
});
observer.observe({ entryTypes: ["navigation"] });
}
}
// First Contentful Paint
measureFCP() {
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === "first-contentful-paint") {
this.metrics.fcp = entry.startTime;
console.log("FCP:", entry.startTime);
this.sendMetric("FCP", entry.startTime);
}
});
});
observer.observe({ entryTypes: ["paint"] });
}
}
// Send metrics to analytics
sendMetric(name, value) {
// Google Analytics 4
if (typeof gtag !== "undefined") {
gtag("event", "web_vital", {
name: name,
value: Math.round(value),
event_category: "Web Vitals",
});
}
// Custom analytics endpoint
fetch("/api/analytics/performance", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
metric: name,
value: value,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
}),
}).catch(console.error);
}
// Get all metrics
getMetrics() {
return this.metrics;
}
}
// Initialize performance monitoring
const performanceMonitor = new PerformanceMonitor();
// Resource timing analysis
function analyzeResourceTiming() {
const resources = performance.getEntriesByType("resource");
const analysis = {
totalResources: resources.length,
totalSize: 0,
totalDuration: 0,
slowestResources: [],
largestResources: [],
resourceTypes: {},
};
resources.forEach((resource) => {
const duration = resource.responseEnd - resource.startTime;
const size = resource.transferSize || 0;
analysis.totalDuration += duration;
analysis.totalSize += size;
// Track by resource type
const type = getResourceType(resource.name);
if (!analysis.resourceTypes[type]) {
analysis.resourceTypes[type] = {
count: 0,
totalSize: 0,
totalDuration: 0,
};
}
analysis.resourceTypes[type].count++;
analysis.resourceTypes[type].totalSize += size;
analysis.resourceTypes[type].totalDuration += duration;
// Track slowest resources
if (duration > 1000) {
// > 1 second
analysis.slowestResources.push({
url: resource.name,
duration: duration,
size: size,
});
}
// Track largest resources
if (size > 100000) {
// > 100KB
analysis.largestResources.push({
url: resource.name,
size: size,
duration: duration,
});
}
});
// Sort by duration and size
analysis.slowestResources.sort((a, b) => b.duration - a.duration);
analysis.largestResources.sort((a, b) => b.size - a.size);
console.log("Resource Analysis:", analysis);
return analysis;
}
function getResourceType(url) {
if (url.match(/\.(css)$/)) return "CSS";
if (url.match(/\.(js)$/)) return "JavaScript";
if (url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)) return "Image";
if (url.match(/\.(woff|woff2|ttf|eot)$/)) return "Font";
if (url.match(/\.(mp4|webm|ogg)$/)) return "Video";
return "Other";
}
2. Performance Budgets
// Performance budget configuration
const PERFORMANCE_BUDGET = {
// Time-based metrics (milliseconds)
lcp: 2500,
fid: 100,
ttfb: 600,
fcp: 1800,
// Size-based metrics (bytes)
totalSize: 2000000, // 2MB
jsSize: 500000, // 500KB
cssSize: 100000, // 100KB
imageSize: 1000000, // 1MB
fontSize: 100000, // 100KB
// Count-based metrics
totalRequests: 50,
jsRequests: 10,
cssRequests: 5,
imageRequests: 20,
// Layout shift
cls: 0.1,
};
class PerformanceBudgetMonitor {
constructor(budget = PERFORMANCE_BUDGET) {
this.budget = budget;
this.violations = [];
}
checkBudget() {
const metrics = performanceMonitor.getMetrics();
const resourceAnalysis = analyzeResourceTiming();
// Check time-based metrics
Object.keys(this.budget).forEach((metric) => {
if (metrics[metric] && metrics[metric] > this.budget[metric]) {
this.violations.push({
type: "time",
metric: metric,
actual: metrics[metric],
budget: this.budget[metric],
severity: this.getSeverity(metrics[metric], this.budget[metric]),
});
}
});
// Check size-based metrics
if (resourceAnalysis.totalSize > this.budget.totalSize) {
this.violations.push({
type: "size",
metric: "totalSize",
actual: resourceAnalysis.totalSize,
budget: this.budget.totalSize,
severity: this.getSeverity(
resourceAnalysis.totalSize,
this.budget.totalSize
),
});
}
// Check resource type sizes
Object.keys(resourceAnalysis.resourceTypes).forEach((type) => {
const budgetKey = type.toLowerCase() + "Size";
if (this.budget[budgetKey]) {
const actualSize = resourceAnalysis.resourceTypes[type].totalSize;
if (actualSize > this.budget[budgetKey]) {
this.violations.push({
type: "size",
metric: budgetKey,
actual: actualSize,
budget: this.budget[budgetKey],
severity: this.getSeverity(actualSize, this.budget[budgetKey]),
});
}
}
});
return this.violations;
}
getSeverity(actual, budget) {
const ratio = actual / budget;
if (ratio > 2) return "critical";
if (ratio > 1.5) return "high";
if (ratio > 1.2) return "medium";
return "low";
}
generateReport() {
const violations = this.checkBudget();
if (violations.length === 0) {
console.log("✅ All performance budgets are within limits!");
return;
}
console.log("⚠️ Performance Budget Violations:");
violations.forEach((violation) => {
const percentage = (
(violation.actual / violation.budget - 1) *
100
).toFixed(1);
console.log(
`${violation.severity.toUpperCase()}: ${violation.metric} is ${percentage}% over budget`
);
console.log(` Actual: ${violation.actual}, Budget: ${violation.budget}`);
});
}
}
// Monitor performance budget
const budgetMonitor = new PerformanceBudgetMonitor();
setTimeout(() => budgetMonitor.generateReport(), 5000);
Image Optimization
1. Modern Image Formats dan Responsive Images
<!-- Modern image formats dengan fallback -->
<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="Description" loading="lazy" />
</picture>
<!-- Responsive images -->
<img
src="image-800w.jpg"
srcset="
image-400w.jpg 400w,
image-800w.jpg 800w,
image-1200w.jpg 1200w,
image-1600w.jpg 1600w
"
sizes="
(max-width: 400px) 100vw,
(max-width: 800px) 50vw,
33vw
"
alt="Responsive image"
loading="lazy"
/>
<!-- Art direction dengan picture -->
<picture>
<source media="(max-width: 799px)" srcset="mobile-image.jpg" />
<source media="(min-width: 800px)" srcset="desktop-image.jpg" />
<img src="desktop-image.jpg" alt="Art directed image" />
</picture>
2. Lazy Loading Implementation
// Advanced lazy loading dengan Intersection Observer
class LazyImageLoader {
constructor(options = {}) {
this.options = {
rootMargin: "50px 0px",
threshold: 0.01,
...options,
};
this.imageObserver = null;
this.images = [];
this.init();
}
init() {
// Check for Intersection Observer support
if ("IntersectionObserver" in window) {
this.imageObserver = new IntersectionObserver(
this.onIntersection.bind(this),
this.options
);
this.observeImages();
} else {
// Fallback for older browsers
this.loadAllImages();
}
}
observeImages() {
const images = document.querySelectorAll(
"img[data-src], picture[data-src]"
);
images.forEach((img) => {
this.imageObserver.observe(img);
this.images.push(img);
});
}
onIntersection(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.imageObserver.unobserve(entry.target);
}
});
}
loadImage(img) {
return new Promise((resolve, reject) => {
const imageElement =
img.tagName === "IMG" ? img : img.querySelector("img");
if (!imageElement) {
reject(new Error("No img element found"));
return;
}
// Create a new image to preload
const newImg = new Image();
newImg.onload = () => {
// Update src attributes
if (img.dataset.src) {
imageElement.src = img.dataset.src;
}
if (img.dataset.srcset) {
imageElement.srcset = img.dataset.srcset;
}
// Handle picture element sources
if (img.tagName === "PICTURE") {
const sources = img.querySelectorAll("source[data-srcset]");
sources.forEach((source) => {
source.srcset = source.dataset.srcset;
delete source.dataset.srcset;
});
}
// Add loaded class for CSS transitions
img.classList.add("loaded");
// Clean up data attributes
delete img.dataset.src;
delete img.dataset.srcset;
resolve(img);
};
newImg.onerror = () => {
img.classList.add("error");
reject(new Error(`Failed to load image: ${img.dataset.src}`));
};
// Start loading
newImg.src = img.dataset.src || imageElement.src;
});
}
loadAllImages() {
const images = document.querySelectorAll(
"img[data-src], picture[data-src]"
);
images.forEach((img) => {
this.loadImage(img).catch(console.error);
});
}
// Progressive image loading
loadProgressiveImage(container, lowQualitySrc, highQualitySrc) {
const img = container.querySelector("img");
// Load low quality first
img.src = lowQualitySrc;
img.classList.add("loading");
// Preload high quality
const highQualityImg = new Image();
highQualityImg.onload = () => {
img.src = highQualitySrc;
img.classList.remove("loading");
img.classList.add("loaded");
};
highQualityImg.src = highQualitySrc;
}
}
// Initialize lazy loading
const lazyLoader = new LazyImageLoader({
rootMargin: "100px 0px",
threshold: 0.1,
});
// CSS for smooth transitions
const lazyLoadingCSS = `
img[data-src] {
opacity: 0;
transition: opacity 0.3s;
}
img.loaded {
opacity: 1;
}
img.loading {
filter: blur(5px);
transition: filter 0.3s;
}
img.error {
opacity: 0.5;
filter: grayscale(100%);
}
`;
// Add CSS to document
const style = document.createElement("style");
style.textContent = lazyLoadingCSS;
document.head.appendChild(style);
3. Image Compression dan Optimization
// Client-side image compression
class ImageCompressor {
constructor(options = {}) {
this.options = {
quality: 0.8,
maxWidth: 1920,
maxHeight: 1080,
format: "image/jpeg",
...options,
};
}
async compressImage(file) {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
// Calculate new dimensions
const { width, height } = this.calculateDimensions(
img.width,
img.height
);
// Set canvas size
canvas.width = width;
canvas.height = height;
// Draw and compress
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error("Canvas to Blob conversion failed"));
}
},
this.options.format,
this.options.quality
);
};
img.onerror = () => reject(new Error("Image load failed"));
img.src = URL.createObjectURL(file);
});
}
calculateDimensions(originalWidth, originalHeight) {
let { width, height } = { width: originalWidth, height: originalHeight };
// Scale down if too large
if (width > this.options.maxWidth) {
height = (height * this.options.maxWidth) / width;
width = this.options.maxWidth;
}
if (height > this.options.maxHeight) {
width = (width * this.options.maxHeight) / height;
height = this.options.maxHeight;
}
return { width: Math.round(width), height: Math.round(height) };
}
async generateMultipleSizes(file, sizes = [400, 800, 1200, 1600]) {
const results = [];
for (const size of sizes) {
const compressor = new ImageCompressor({
...this.options,
maxWidth: size,
maxHeight: size,
});
try {
const compressed = await compressor.compressImage(file);
results.push({
size: size,
blob: compressed,
sizeInBytes: compressed.size,
});
} catch (error) {
console.error(`Failed to compress image at size ${size}:`, error);
}
}
return results;
}
}
// Usage example
const compressor = new ImageCompressor({
quality: 0.85,
maxWidth: 1920,
format: "image/webp",
});
// Handle file input
document.getElementById("imageInput").addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
console.log(`Original size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
const compressed = await compressor.compressImage(file);
console.log(
`Compressed size: ${(compressed.size / 1024 / 1024).toFixed(2)} MB`
);
const reduction = (
((file.size - compressed.size) / file.size) *
100
).toFixed(1);
console.log(`Size reduction: ${reduction}%`);
// Generate multiple sizes
const multipleSizes = await compressor.generateMultipleSizes(file);
console.log("Multiple sizes generated:", multipleSizes);
} catch (error) {
console.error("Compression failed:", error);
}
});
JavaScript Optimization
1. Code Splitting dan Lazy Loading
// Dynamic imports untuk code splitting
class ModuleLoader {
constructor() {
this.loadedModules = new Map();
this.loadingPromises = new Map();
}
async loadModule(moduleName) {
// Return cached module if already loaded
if (this.loadedModules.has(moduleName)) {
return this.loadedModules.get(moduleName);
}
// Return existing promise if already loading
if (this.loadingPromises.has(moduleName)) {
return this.loadingPromises.get(moduleName);
}
// Start loading module
const loadingPromise = this.importModule(moduleName);
this.loadingPromises.set(moduleName, loadingPromise);
try {
const module = await loadingPromise;
this.loadedModules.set(moduleName, module);
this.loadingPromises.delete(moduleName);
return module;
} catch (error) {
this.loadingPromises.delete(moduleName);
throw error;
}
}
async importModule(moduleName) {
const moduleMap = {
chart: () => import("./modules/chart.js"),
calendar: () => import("./modules/calendar.js"),
editor: () => import("./modules/editor.js"),
analytics: () => import("./modules/analytics.js"),
payment: () => import("./modules/payment.js"),
};
if (!moduleMap[moduleName]) {
throw new Error(`Module '${moduleName}' not found`);
}
return moduleMap[moduleName]();
}
// Preload modules based on user behavior
preloadModule(moduleName) {
// Use requestIdleCallback for non-critical preloading
if ("requestIdleCallback" in window) {
requestIdleCallback(() => {
this.loadModule(moduleName).catch(console.error);
});
} else {
setTimeout(() => {
this.loadModule(moduleName).catch(console.error);
}, 100);
}
}
// Preload based on intersection observer
preloadOnHover(element, moduleName) {
let preloaded = false;
const preload = () => {
if (!preloaded) {
preloaded = true;
this.preloadModule(moduleName);
}
};
element.addEventListener("mouseenter", preload, { once: true });
element.addEventListener("touchstart", preload, { once: true });
}
}
const moduleLoader = new ModuleLoader();
// Usage examples
// Load module when needed
document.getElementById("showChart").addEventListener("click", async () => {
try {
const chartModule = await moduleLoader.loadModule("chart");
chartModule.createChart("#chart-container", data);
} catch (error) {
console.error("Failed to load chart module:", error);
}
});
// Preload on hover
const chartButton = document.getElementById("showChart");
moduleLoader.preloadOnHover(chartButton, "chart");
// Route-based code splitting
class Router {
constructor() {
this.routes = new Map();
this.currentRoute = null;
this.moduleLoader = new ModuleLoader();
}
addRoute(path, moduleFactory) {
this.routes.set(path, moduleFactory);
}
async navigate(path) {
if (this.currentRoute === path) return;
try {
// Show loading state
this.showLoading();
// Load route module
const moduleFactory = this.routes.get(path);
if (!moduleFactory) {
throw new Error(`Route '${path}' not found`);
}
const module = await moduleFactory();
// Render route
await this.renderRoute(module, path);
this.currentRoute = path;
this.hideLoading();
} catch (error) {
console.error("Navigation failed:", error);
this.showError(error);
}
}
async renderRoute(module, path) {
const container = document.getElementById("app");
// Cleanup previous route
if (this.currentRouteCleanup) {
this.currentRouteCleanup();
}
// Render new route
this.currentRouteCleanup = await module.render(container);
// Update URL
history.pushState({ path }, "", path);
}
showLoading() {
document.getElementById("loading").style.display = "block";
}
hideLoading() {
document.getElementById("loading").style.display = "none";
}
showError(error) {
const errorElement = document.getElementById("error");
errorElement.textContent = error.message;
errorElement.style.display = "block";
}
}
// Setup router
const router = new Router();
router.addRoute("/", () => import("./pages/home.js"));
router.addRoute("/about", () => import("./pages/about.js"));
router.addRoute("/products", () => import("./pages/products.js"));
router.addRoute("/contact", () => import("./pages/contact.js"));
// Handle navigation
window.addEventListener("popstate", (e) => {
const path = e.state?.path || "/";
router.navigate(path);
});
// Handle link clicks
document.addEventListener("click", (e) => {
if (e.target.matches("a[data-route]")) {
e.preventDefault();
const path = e.target.getAttribute("href");
router.navigate(path);
}
});
2. Bundle Optimization
// webpack.config.js - Advanced optimization
const path = require("path");
const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
vendor: ["react", "react-dom", "lodash"],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].chunk.js",
clean: true,
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log", "console.info"],
},
mangle: {
safari10: true,
},
format: {
comments: false,
},
},
extractComments: false,
}),
],
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
priority: 10,
},
common: {
name: "common",
minChunks: 2,
chunks: "all",
priority: 5,
reuseExistingChunk: true,
},
},
},
runtimeChunk: {
name: "runtime",
},
},
plugins: [
// Gzip compression
new CompressionPlugin({
algorithm: "gzip",
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
// Brotli compression
new CompressionPlugin({
filename: "[path][base].br",
algorithm: "brotliCompress",
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11,
},
threshold: 8192,
minRatio: 0.8,
}),
// Bundle analyzer (only in analyze mode)
process.env.ANALYZE && new BundleAnalyzerPlugin(),
// Define environment variables
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
}),
].filter(Boolean),
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: {
browsers: ["> 1%", "last 2 versions"],
},
modules: false,
useBuiltIns: "usage",
corejs: 3,
},
],
],
plugins: ["@babel/plugin-syntax-dynamic-import"],
},
},
},
],
},
};
Caching Strategies
1. HTTP Caching
// Service Worker untuk advanced caching
class CacheManager {
constructor() {
this.CACHE_NAME = "app-cache-v1";
this.STATIC_CACHE = "static-cache-v1";
this.DYNAMIC_CACHE = "dynamic-cache-v1";
this.API_CACHE = "api-cache-v1";
this.STATIC_ASSETS = [
"/",
"/css/main.css",
"/js/main.js",
"/images/logo.svg",
"/manifest.json",
];
this.API_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
this.STATIC_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
}
async install() {
const cache = await caches.open(this.STATIC_CACHE);
return cache.addAll(this.STATIC_ASSETS);
}
async handleFetch(event) {
const { request } = event;
const url = new URL(request.url);
// Handle API requests
if (url.pathname.startsWith("/api/")) {
return this.handleApiRequest(request);
}
// Handle static assets
if (this.isStaticAsset(request)) {
return this.handleStaticAsset(request);
}
// Handle navigation requests
if (request.mode === "navigate") {
return this.handleNavigation(request);
}
// Default: network first
return this.networkFirst(request);
}
async handleApiRequest(request) {
const cache = await caches.open(this.API_CACHE);
const cachedResponse = await cache.match(request);
// Check if cached response is still fresh
if (cachedResponse) {
const cachedDate = new Date(cachedResponse.headers.get("date"));
const now = new Date();
if (now - cachedDate < this.API_CACHE_DURATION) {
return cachedResponse;
}
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Clone response before caching
const responseClone = networkResponse.clone();
cache.put(request, responseClone);
}
return networkResponse;
} catch (error) {
// Return cached response if network fails
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
async handleStaticAsset(request) {
const cache = await caches.open(this.STATIC_CACHE);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const responseClone = networkResponse.clone();
cache.put(request, responseClone);
}
return networkResponse;
} catch (error) {
// Return offline fallback for images
if (request.destination === "image") {
return caches.match("/images/offline-fallback.svg");
}
throw error;
}
}
async handleNavigation(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(this.DYNAMIC_CACHE);
const responseClone = networkResponse.clone();
cache.put(request, responseClone);
}
return networkResponse;
} catch (error) {
// Return cached page or offline fallback
const cache = await caches.open(this.DYNAMIC_CACHE);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
return caches.match("/offline.html");
}
}
async networkFirst(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(this.DYNAMIC_CACHE);
const responseClone = networkResponse.clone();
cache.put(request, responseClone);
}
return networkResponse;
} catch (error) {
const cache = await caches.open(this.DYNAMIC_CACHE);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
isStaticAsset(request) {
const url = new URL(request.url);
return url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/);
}
async cleanupOldCaches() {
const cacheNames = await caches.keys();
const currentCaches = [
this.CACHE_NAME,
this.STATIC_CACHE,
this.DYNAMIC_CACHE,
this.API_CACHE,
];
return Promise.all(
cacheNames
.filter((cacheName) => !currentCaches.includes(cacheName))
.map((cacheName) => caches.delete(cacheName))
);
}
}
// Service Worker implementation
const cacheManager = new CacheManager();
self.addEventListener("install", (event) => {
event.waitUntil(
cacheManager.install().then(() => {
return self.skipWaiting();
})
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
cacheManager.cleanupOldCaches().then(() => {
return self.clients.claim();
})
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(cacheManager.handleFetch(event));
});
2. Browser Caching Headers
// Express.js server dengan optimal caching headers
const express = require("express");
const compression = require("compression");
const helmet = require("helmet");
const path = require("path");
const app = express();
// Security headers
app.use(helmet());
// Compression
app.use(
compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
if (req.headers["x-no-compression"]) {
return false;
}
return compression.filter(req, res);
},
})
);
// Cache control middleware
const setCacheHeaders = (duration, type = "public") => {
return (req, res, next) => {
res.set({
"Cache-Control": `${type}, max-age=${duration}`,
Expires: new Date(Date.now() + duration * 1000).toUTCString(),
});
next();
};
};
// Static assets with long-term caching
app.use(
"/static",
setCacheHeaders(31536000), // 1 year
express.static(path.join(__dirname, "public/static"), {
etag: true,
lastModified: true,
immutable: true,
})
);
// Images with medium-term caching
app.use(
"/images",
setCacheHeaders(2592000), // 30 days
express.static(path.join(__dirname, "public/images"))
);
// API routes with short-term caching
app.get(
"/api/data",
setCacheHeaders(300), // 5 minutes
(req, res) => {
res.json({ data: "cached for 5 minutes" });
}
);
// API routes with conditional caching
app.get("/api/user/:id", (req, res) => {
const userId = req.params.id;
const userData = getUserData(userId);
// Set ETag for conditional requests
const etag = generateETag(userData);
res.set("ETag", etag);
// Check if client has current version
if (req.headers["if-none-match"] === etag) {
return res.status(304).end();
}
res.set("Cache-Control", "private, max-age=300");
res.json(userData);
});
// HTML pages with no-cache
app.get("*", setCacheHeaders(0, "no-cache"), (req, res) => {
res.sendFile(path.join(__dirname, "public/index.html"));
});
function generateETag(data) {
const crypto = require("crypto");
return crypto.createHash("md5").update(JSON.stringify(data)).digest("hex");
}
function getUserData(userId) {
// Simulate database query
return {
id: userId,
name: "John Doe",
lastModified: new Date().toISOString(),
};
}
app.listen(3000, () => {
console.log("Server running on port 3000");
});
Critical Rendering Path Optimization
1. CSS Optimization
<!-- Critical CSS inline -->
<style>
/* Above-the-fold styles */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.header {
background: #fff;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.hero {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
}
.loading {
display: none;
}
</style>
<!-- Preload critical resources -->
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="preload" href="/css/critical.css" as="style" />
<link rel="preload" href="/js/main.js" as="script" />
<!-- Load non-critical CSS asynchronously -->
<link
rel="preload"
href="/css/non-critical.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/css/non-critical.css" /></noscript>
// Critical CSS extraction
class CriticalCSSExtractor {
constructor() {
this.criticalSelectors = new Set();
this.observer = null;
}
init() {
// Extract above-the-fold selectors
this.extractAboveFoldSelectors();
// Monitor for new critical elements
this.observeNewElements();
}
extractAboveFoldSelectors() {
const viewportHeight = window.innerHeight;
const elements = document.querySelectorAll("*");
elements.forEach((element) => {
const rect = element.getBoundingClientRect();
// Check if element is above the fold
if (rect.top < viewportHeight && rect.bottom > 0) {
this.addCriticalSelectors(element);
}
});
}
addCriticalSelectors(element) {
// Add element's classes
element.classList.forEach((className) => {
this.criticalSelectors.add(`.${className}`);
});
// Add element's ID
if (element.id) {
this.criticalSelectors.add(`#${element.id}`);
}
// Add tag name for basic elements
const tagName = element.tagName.toLowerCase();
if (
["body", "header", "nav", "main", "footer", "h1", "h2", "h3"].includes(
tagName
)
) {
this.criticalSelectors.add(tagName);
}
}
observeNewElements() {
if ("IntersectionObserver" in window) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.addCriticalSelectors(entry.target);
}
});
},
{ rootMargin: "0px 0px -50% 0px" }
);
// Observe all elements
document.querySelectorAll("*").forEach((element) => {
this.observer.observe(element);
});
}
}
generateCriticalCSS() {
const allStyleSheets = Array.from(document.styleSheets);
let criticalCSS = "";
allStyleSheets.forEach((styleSheet) => {
try {
const rules = Array.from(styleSheet.cssRules || styleSheet.rules);
rules.forEach((rule) => {
if (rule.type === CSSRule.STYLE_RULE) {
const selector = rule.selectorText;
// Check if selector is critical
if (this.isCriticalSelector(selector)) {
criticalCSS += rule.cssText + "\n";
}
}
});
} catch (e) {
// Handle CORS issues with external stylesheets
console.warn("Cannot access stylesheet:", styleSheet.href);
}
});
return criticalCSS;
}
isCriticalSelector(selector) {
// Simple check - in production, use more sophisticated matching
return Array.from(this.criticalSelectors).some((criticalSelector) =>
selector.includes(criticalSelector)
);
}
async loadNonCriticalCSS() {
const nonCriticalLinks = document.querySelectorAll(
"link[data-non-critical]"
);
nonCriticalLinks.forEach((link) => {
const href = link.getAttribute("data-href");
if (href) {
const newLink = document.createElement("link");
newLink.rel = "stylesheet";
newLink.href = href;
document.head.appendChild(newLink);
}
});
}
}
// Initialize critical CSS extraction
const criticalExtractor = new CriticalCSSExtractor();
criticalExtractor.init();
// Load non-critical CSS after page load
window.addEventListener("load", () => {
requestIdleCallback(() => {
criticalExtractor.loadNonCriticalCSS();
});
});
2. Resource Prioritization
<!-- Resource hints -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//api.example.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Preload critical resources -->
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="preload" href="/images/hero.webp" as="image" />
<link rel="preload" href="/api/initial-data" as="fetch" crossorigin />
<!-- Prefetch likely next resources -->
<link rel="prefetch" href="/js/secondary.js" />
<link rel="prefetch" href="/pages/about.html" />
<!-- Module preload for ES modules -->
<link rel="modulepreload" href="/js/modules/main.js" />
// Intelligent resource preloading
class ResourcePreloader {
constructor() {
this.preloadedResources = new Set();
this.userBehavior = {
hoveredLinks: new Set(),
clickedElements: new Set(),
scrollDepth: 0,
};
this.init();
}
init() {
this.trackUserBehavior();
this.preloadBasedOnBehavior();
this.preloadBasedOnViewport();
}
trackUserBehavior() {
// Track link hovers
document.addEventListener("mouseover", (e) => {
if (e.target.tagName === "A") {
this.userBehavior.hoveredLinks.add(e.target.href);
this.preloadResource(e.target.href, "document");
}
});
// Track scroll depth
let ticking = false;
document.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
const scrollPercent =
(window.scrollY /
(document.body.scrollHeight - window.innerHeight)) *
100;
this.userBehavior.scrollDepth = Math.max(
this.userBehavior.scrollDepth,
scrollPercent
);
// Preload resources based on scroll depth
if (scrollPercent > 50) {
this.preloadFooterResources();
}
ticking = false;
});
ticking = true;
}
});
// Track element interactions
document.addEventListener("click", (e) => {
this.userBehavior.clickedElements.add(e.target);
this.predictNextResources(e.target);
});
}
preloadResource(url, type = "fetch") {
if (this.preloadedResources.has(url)) return;
const link = document.createElement("link");
link.rel = "prefetch";
link.href = url;
if (type !== "document") {
link.as = type;
}
if (type === "fetch") {
link.crossOrigin = "anonymous";
}
document.head.appendChild(link);
this.preloadedResources.add(url);
console.log(`Preloaded: ${url} (${type})`);
}
preloadBasedOnViewport() {
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.preloadElementResources(entry.target);
}
});
},
{ rootMargin: "200px" }
);
// Observe images and iframes
document
.querySelectorAll("img[data-src], iframe[data-src]")
.forEach((element) => {
observer.observe(element);
});
// Observe sections that might contain dynamic content
document.querySelectorAll("[data-preload]").forEach((element) => {
observer.observe(element);
});
}
}
preloadElementResources(element) {
// Preload images
if (element.tagName === "IMG" && element.dataset.src) {
this.preloadResource(element.dataset.src, "image");
}
// Preload iframe content
if (element.tagName === "IFRAME" && element.dataset.src) {
this.preloadResource(element.dataset.src, "document");
}
// Preload data for sections
if (element.dataset.preload) {
this.preloadResource(element.dataset.preload, "fetch");
}
}
predictNextResources(element) {
// Predict based on element type and context
if (element.classList.contains("product-card")) {
// User clicked on product, preload product details page
const productId = element.dataset.productId;
if (productId) {
this.preloadResource(`/api/products/${productId}`, "fetch");
this.preloadResource(`/products/${productId}`, "document");
}
}
if (element.classList.contains("category-link")) {
// User clicked on category, preload category page
const categorySlug = element.dataset.category;
if (categorySlug) {
this.preloadResource(
`/api/categories/${categorySlug}/products`,
"fetch"
);
this.preloadResource(`/categories/${categorySlug}`, "document");
}
}
}
preloadFooterResources() {
// Preload resources typically accessed from footer
const footerLinks = ["/about", "/contact", "/privacy", "/terms"];
footerLinks.forEach((link) => {
this.preloadResource(link, "document");
});
}
// Preload based on machine learning predictions
async preloadWithML() {
try {
const userSession = {
currentPage: window.location.pathname,
timeOnPage: Date.now() - performance.timing.navigationStart,
scrollDepth: this.userBehavior.scrollDepth,
hoveredLinks: Array.from(this.userBehavior.hoveredLinks),
deviceType: this.getDeviceType(),
};
const response = await fetch("/api/ml/predict-next-resources", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userSession),
});
const predictions = await response.json();
predictions.forEach((prediction) => {
if (prediction.confidence > 0.7) {
this.preloadResource(prediction.url, prediction.type);
}
});
} catch (error) {
console.error("ML prediction failed:", error);
}
}
getDeviceType() {
const width = window.innerWidth;
if (width < 768) return "mobile";
if (width < 1024) return "tablet";
return "desktop";
}
}
// Initialize resource preloader
const resourcePreloader = new ResourcePreloader();
// Run ML predictions after initial load
window.addEventListener("load", () => {
setTimeout(() => {
resourcePreloader.preloadWithML();
}, 2000);
});
Kesimpulan
Web performance optimization adalah proses berkelanjutan yang memerlukan pendekatan holistik. Dengan menerapkan teknik-teknik yang telah dibahas:
Key Takeaways
- Measure First: Selalu ukur performa sebelum dan sesudah optimisasi
- Focus on User Experience: Prioritaskan Core Web Vitals dan user-centric metrics
- Optimize Critical Path: Prioritaskan resource yang dibutuhkan untuk first paint
- Implement Smart Caching: Gunakan strategi caching yang tepat untuk setiap jenis resource
- Monitor Continuously: Setup monitoring untuk mendeteksi regresi performa
Performance Checklist
- ✅ Implement performance monitoring
- ✅ Optimize images (format, compression, lazy loading)
- ✅ Minimize and compress JavaScript/CSS
- ✅ Implement code splitting
- ✅ Setup proper caching headers
- ✅ Use Service Worker for offline support
- ✅ Optimize critical rendering path
- ✅ Implement resource preloading
- ✅ Monitor Core Web Vitals
- ✅ Setup performance budgets
Performa website yang optimal tidak hanya meningkatkan user experience, tetapi juga berdampak langsung pada business metrics seperti conversion rate, SEO ranking, dan user retention.
Dengan menerapkan strategi yang tepat dan monitoring yang berkelanjutan, Anda dapat menciptakan website yang cepat, responsif, dan memberikan pengalaman terbaik bagi pengguna di semua perangkat dan kondisi jaringan.
Resources dan Tools
Performance Testing:
- Google PageSpeed Insights
- WebPageTest
- Lighthouse
- GTmetrix
Monitoring:
- Google Analytics 4 (Web Vitals)
- New Relic
- DataDog
- Pingdom
Development Tools:
- Chrome DevTools
- Webpack Bundle Analyzer
- Source Map Explorer
- Performance Observer API
Ingat bahwa performance optimization adalah marathon, bukan sprint. Mulai dengan quick wins, ukur dampaknya, dan terus iterasi untuk mencapai performa optimal yang berkelanjutan.



