阿里云主机折上折
  • 微信号
Current Site:Index > Progressive Web App (PWA) optimization

Progressive Web App (PWA) optimization

Author:Chuan Chen 阅读数:18994人阅读 分类: 性能优化

Progressive Web App (PWA) Optimization

PWAs combine the advantages of web and native applications, delivering a web experience closer to native apps. The key to optimizing PWAs lies in improving performance, reliability, and user experience, covering aspects such as resource loading, caching strategies, background synchronization, and more.

Resource Loading Optimization

Resource loading speed directly impacts the first-screen rendering time of a PWA. The following strategies can significantly enhance performance:

  1. Code Splitting and Lazy Loading
    Use dynamic import() to achieve on-demand loading and reduce the initial bundle size:

    // Dynamically load non-critical modules
    button.addEventListener('click', () => {
      import('./analytics.js').then(module => {
        module.trackEvent('click');
      });
    });
    
  2. Preloading Critical Resources
    Preload critical CSS and fonts in the HTML head:

    <link rel="preload" href="/styles/main.css" as="style">
    <link rel="preload" href="/fonts/roboto.woff2" as="font" crossorigin>
    
  3. HTTP/2 Server Push
    Configure the server to proactively push critical resources, reducing round-trip time (RTT). Nginx example:

    http2_push /styles/main.css;
    http2_push /scripts/app.js;
    

Service Worker Caching Strategies

Service Worker is the core of a PWA. A well-designed caching strategy enhances the offline experience:

  1. Layered Caching Strategy

    • Precache: Static resources determined at build time (e.g., CSS, JS)
    • Runtime Cache: Dynamic content (e.g., API responses, user data)
    const PRECACHE = 'precache-v1';
    const RUNTIME = 'runtime';
    
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open(PRECACHE).then(cache => 
          cache.addAll([
            '/',
            '/styles/main.css',
            '/scripts/app.js'
          ])
        )
      );
    });
    
    self.addEventListener('fetch', event => {
      if (event.request.url.startsWith('https://api.example.com')) {
        event.respondWith(
          caches.open(RUNTIME).then(cache => 
            fetch(event.request).then(response => {
              cache.put(event.request, response.clone());
              return response;
            }).catch(() => caches.match(event.request))
          )
        );
      } else {
        event.respondWith(
          caches.match(event.request).then(response => 
            response || fetch(event.request)
          )
        );
      }
    });
    
  2. Cache Update Mechanism
    Use versioned cache names and the activate event to clean up old caches:

    self.addEventListener('activate', event => {
      const cacheWhitelist = [PRECACHE, RUNTIME];
      event.waitUntil(
        caches.keys().then(keyList =>
          Promise.all(keyList.map(key => {
            if (!cacheWhitelist.includes(key)) {
              return caches.delete(key);
            }
          }))
        )
      );
    });
    

Network Request Optimization

  1. Background Sync
    Automatically retry failed requests when the network is restored:

    self.addEventListener('sync', event => {
      if (event.tag === 'sync-comments') {
        event.waitUntil(
          sendFailedComments().then(() => 
            showNotification('Comments synced')
          )
        );
      }
    });
    
    // Register sync task
    navigator.serviceWorker.ready.then(registration => {
      registration.sync.register('sync-comments');
    });
    
  2. Request Fallback Handling
    Implement graceful degradation for API requests:

    function fetchWithFallback(url) {
      return fetch(url)
        .catch(() => fetchFromCache(url))
        .catch(() => getPlaceholderData());
    }
    

Rendering Performance Optimization

  1. Virtual List for Long Lists
    Use Intersection Observer to implement dynamic rendering:

    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          renderItem(entry.target.dataset.index);
          observer.unobserve(entry.target);
        }
      });
    });
    
    items.forEach((item, index) => {
      const placeholder = document.createElement('div');
      placeholder.dataset.index = index;
      observer.observe(placeholder);
      list.appendChild(placeholder);
    });
    
  2. Web Worker for Heavy Computations
    Offload CPU-intensive tasks from the main thread:

    // main.js
    const worker = new Worker('compute.js');
    worker.postMessage({data: largeDataSet});
    worker.onmessage = e => updateUI(e.data);
    
    // compute.js
    self.onmessage = e => {
      const result = heavyComputation(e.data);
      self.postMessage(result);
    };
    

App Shell Architecture

  1. Minimal Initial HTML
    Include only the critical skeleton structure:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>App Shell</title>
      <link rel="manifest" href="/manifest.json">
      <style>
        /* Inline critical CSS */
        .header { position: fixed; top: 0; }
        .skeleton { animation: pulse 1.5s infinite; }
      </style>
    </head>
    <body>
      <div class="header"></div>
      <div class="content">
        <div class="skeleton"></div>
      </div>
      <script src="/app.js" async></script>
    </body>
    </html>
    
  2. Dynamic Content Injection
    Populate content via JavaScript:

    window.addEventListener('DOMContentLoaded', () => {
      fetchContent().then(data => {
        document.querySelector('.content').innerHTML = 
          renderPosts(data.posts);
      });
    });
    

Performance Monitoring and Tuning

  1. Key Metrics Collection
    Use PerformanceObserver to monitor core metrics:

    const observer = new PerformanceObserver(list => {
      for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
          console.log('FCP:', entry.startTime);
          sendToAnalytics({metric: 'FCP', value: entry.startTime});
        }
      }
    });
    observer.observe({type: 'paint', buffered: true});
    
  2. Memory Leak Detection
    Periodically check memory usage:

    setInterval(() => {
      const memory = performance.memory;
      if (memory.usedJSHeapSize > memory.jsHeapSizeLimit * 0.7) {
        console.warn('Memory usage high:', memory);
      }
    }, 10000);
    

Offline Experience Enhancement

  1. Custom Offline Pages
    Provide meaningful offline states for different routes:

    self.addEventListener('fetch', event => {
      if (event.request.mode === 'navigate') {
        event.respondWith(
          fetch(event.request).catch(() => 
            caches.match('/offline.html')
          )
        );
      }
    });
    
  2. IndexedDB Data Persistence
    Implement a complete offline data layer:

    function openDB() {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open('appDB', 2);
        request.onupgradeneeded = e => {
          const db = e.target.result;
          if (!db.objectStoreNames.contains('posts')) {
            db.createObjectStore('posts', {keyPath: 'id'});
          }
        };
        request.onsuccess = e => resolve(e.target.result);
        request.onerror = reject;
      });
    }
    
    async function savePost(post) {
      const db = await openDB();
      const tx = db.transaction('posts', 'readwrite');
      tx.objectStore('posts').put(post);
      return tx.complete;
    }
    

Push Notification Optimization

  1. Permission Request Timing
    Request notification permission after user interaction:

    document.getElementById('notify-btn').addEventListener('click', () => {
      Notification.requestPermission().then(permission => {
        if (permission === 'granted') {
          showThankYouMessage();
        }
      });
    });
    
  2. Personalized Notification Content
    Use server push for dynamic content:

    self.addEventListener('push', event => {
      const data = event.data.json();
      event.waitUntil(
        self.registration.showNotification(data.title, {
          body: data.message,
          icon: '/icons/notification.png',
          data: {url: data.link}
        })
      );
    });
    
    self.addEventListener('notificationclick', event => {
      event.notification.close();
      event.waitUntil(
        clients.openWindow(event.notification.data.url)
      );
    });
    

Build Optimization

  1. Modern JavaScript Bundling
    Configure webpack to generate both modern and legacy bundles:

    // webpack.config.js
    module.exports = [{
      entry: './app.js',
      output: {
        filename: 'app.legacy.js',
        path: path.resolve(__dirname, 'dist')
      },
      target: ['web', 'es5']
    }, {
      entry: './app.js',
      output: {
        filename: 'app.modern.js',
        path: path.resolve(__dirname, 'dist')
      },
      target: ['web', 'es2017']
    }];
    
  2. Resource Hashing
    Ensure cache update validity:

    output: {
      filename: '[name].[contenthash:8].js',
      path: path.resolve(__dirname, 'dist')
    }
    

Security Enhancements

  1. Content Security Policy (CSP)
    Prevent XSS attacks:

    <meta http-equiv="Content-Security-Policy" 
          content="default-src 'self'; 
                   script-src 'self' 'unsafe-inline' https://cdn.example.com;
                   style-src 'self' 'unsafe-inline';
                   img-src 'self' data: https://*.example.com">
    
  2. HTTPS Enforcement
    Verify request security in the Service Worker:

    if (!location.protocol.startsWith('https') && 
        !location.hostname.includes('localhost')) {
      throw new Error('PWA requires HTTPS in production');
    }
    

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.