Web Performance Optimization: Panduan Lengkap untuk Website yang Cepat

Pelajari teknik-teknik advanced untuk mengoptimasi performa website, dari Core Web Vitals hingga advanced caching strategies dan modern optimization techniques.

26 Februari 2024
Novin Ardian Yulianto
PerformanceWeb OptimizationCore Web VitalsCachingJavaScript
Web Performance Optimization: Panduan Lengkap untuk Website yang Cepat

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

  1. User Experience: 53% pengguna meninggalkan website jika loading time > 3 detik
  2. SEO Ranking: Google menggunakan Core Web Vitals sebagai ranking factor
  3. Conversion Rate: Setiap 100ms delay dapat menurunkan conversion hingga 7%
  4. Revenue: Amazon kehilangan $1.6 miliar per tahun untuk setiap 100ms delay
  5. Mobile Users: 70% koneksi mobile masih 3G atau lebih lambat

Core Web Vitals

Google menggunakan tiga metrik utama untuk mengukur user experience:

  1. Largest Contentful Paint (LCP): ≤ 2.5 detik
  2. First Input Delay (FID): ≤ 100 milidetik
  3. 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

  1. Measure First: Selalu ukur performa sebelum dan sesudah optimisasi
  2. Focus on User Experience: Prioritaskan Core Web Vitals dan user-centric metrics
  3. Optimize Critical Path: Prioritaskan resource yang dibutuhkan untuk first paint
  4. Implement Smart Caching: Gunakan strategi caching yang tepat untuk setiap jenis resource
  5. 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.

Mari Mulai Kolaborasi.

Ide digital Anda siap menjadi produk nyata. Konsultasikan gratis dengan tim kami dan wujudkan solusi yang tepat untuk bisnis Anda.

Kami menggunakan cookie
Kami menggunakan cookie untuk memastikan Anda mendapatkan pengalaman terbaik di website kami. Untuk informasi lebih lanjut tentang penggunaan cookie, silakan lihat kebijakan cookie kami.

Dengan mengklik "Terima", Anda menyetujui penggunaan cookie kami.

Pelajari lebih lanjut