前端性能优化-第四篇(资源加载与网络优化)

63 阅读5分钟

摘要: 用户感知的加载速度,很大程度上取决于资源如何被下载、解析和执行。优化资源加载是性能提升中最直接有效的一环,直接影响核心性能指标。

1. 关键渲染路径(Critical Rendering Path)优化

关键渲染路径是浏览器将HTML、CSS和JavaScript转换为像素 on 屏幕所经过的步骤序列。

优化策略:

  • 精简和压缩资源

    # 使用工具压缩CSS/JS
    npm install -g uglify-js clean-css
    
    # 压缩JavaScript
    uglifyjs source.js -o output.min.js -c -m
    
    # 压缩CSS  
    cleancss -o style.min.css style.css
    
  • 开启Gzip/Brotli压缩

    # Nginx配置示例
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
    gzip_min_length 1000;
    gzip_comp_level 6;
    
    # 更高效的Brotli压缩(需要单独模块)
    brotli on;
    brotli_types text/plain text/css application/json application/javascript text/xml;
    brotli_comp_level 6;
    
  • 内联关键CSS(Critical CSS)

    <!DOCTYPE html>
    <html>
    <head>
      <style>
        /* 内联首屏内容所需的关键CSS */
        .header { background: #333; color: white; }
        .hero { font-size: 2rem; margin: 2rem 0; }
        /* 其余非关键CSS异步加载 */
      </style>
      <link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
      <noscript><link rel="stylesheet" href="non-critical.css"></noscript>
    </head>
    <body>
      <!-- 页面内容 -->
      <script>
        // 异步加载CSS的降级方案
        !function(e){"use strict"
        var n=function(n,t,o){function i(e){return f.body?e():void setTimeout(function(){i(e)})}var d=!1,r=function(){if(!d){d=!0;var e=t.createElement("link");e.rel="stylesheet",e.href=n,o&&(e.media="only x"),t.head.appendChild(e)}};i(r)};e.loadCSS=n}(this);
        loadCSS("non-critical.css");
      </script>
    </body>
    </html>
    

2. async 与 defer 属性的深入理解

这两个属性都能实现脚本的异步加载,但执行时机有重要区别。

执行时机对比:

<!-- 常规脚本:阻塞HTML解析 -->
<script src="script.js"></script>

<!-- async:下载不阻塞,但下载完立即执行(执行时可能阻塞) -->
<script async src="async-script.js"></script>

<!-- defer:下载不阻塞,在DOMContentLoaded前按顺序执行 -->
<script defer src="defer-script.js"></script>

可视化执行流程:

text

HTML解析开始
    ↓
遇到 <script>        → 下载 → 执行 → 继续解析
    ↓
遇到 <script async>  → 异步下载 → 下载完成立即执行(可能中断解析)
    ↓  
遇到 <script defer>  → 异步下载 → 等待解析完成 → 按顺序执行
    ↓
DOMContentLoaded

最佳实践示例:

<!-- 分析工具、广告脚本等无依赖的脚本 -->
<script async src="https://www.google-analytics.com/analytics.js"></script>

<!-- 有依赖关系或操作DOM的脚本 -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>

<!-- 需要立即执行的内联脚本 -->
<script>
  // 设置全局变量或立即需要的功能
  window.APP_CONFIG = { apiUrl: '/api' };
</script>

3. 现代图片优化技术

图片格式选择指南:

<!-- WebP现代格式(优先) -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="描述">
</picture>

<!-- SVG矢量图标 -->
<svg width="100" height="100">
  <circle cx="50" cy="50" r="40" fill="#ff0000"/>
</svg>

<!-- 响应式图片 -->
<img 
  srcset="image-320w.jpg 320w,
          image-480w.jpg 480w,
          image-800w.jpg 800w"
  sizes="(max-width: 320px) 280px,
         (max-width: 480px) 440px,
         800px"
  src="image-800w.jpg" 
  alt="响应式图片">

图片懒加载实现:

<!-- 原生懒加载(现代浏览器) -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="图片">

<!-- 兼容性更好的懒加载 -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
    
    if ('IntersectionObserver' in window) {
      const lazyImageObserver = new IntersectionObserver(function(entries, observer) {
        entries.forEach(function(entry) {
          if (entry.isIntersecting) {
            const lazyImage = entry.target;
            lazyImage.src = lazyImage.dataset.src;
            lazyImage.classList.remove('lazy');
            lazyImageObserver.unobserve(lazyImage);
          }
        });
      });
      
      lazyImages.forEach(function(lazyImage) {
        lazyImageObserver.observe(lazyImage);
      });
    } else {
      // 回退方案:直接加载所有图片
      lazyImages.forEach(function(lazyImage) {
        lazyImage.src = lazyImage.dataset.src;
      });
    }
  });
</script>

4. 深入理解浏览器缓存策略

HTTP缓存头配置详解:

# 静态资源(强缓存)
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    # 设置1年强缓存(文件名带哈希时可放心设置)
    expires 1y;
    add_header Cache-Control "public, immutable, max-age=31536000";
    
    # 添加ETag进行协商缓存
    etag on;
}

# HTML文件(协商缓存)
location ~* .html$ {
    # 禁用强缓存,使用协商缓存
    expires 0;
    add_header Cache-Control "no-cache, must-revalidate";
    etag on;
}

# API响应(通常不缓存或短时间缓存)
location /api/ {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    expires 0;
}

Service Worker缓存策略:

// sw.js - Service Worker缓存示例
const CACHE_NAME = 'v1.0.0';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/images/logo.png'
];

// 安装阶段:缓存核心资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// 拦截请求:网络优先,失败时使用缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        // 网络请求成功,更新缓存
        const responseClone = response.clone();
        caches.open(CACHE_NAME)
          .then(cache => cache.put(event.request, responseClone));
        return response;
      })
      .catch(() => {
        // 网络失败,使用缓存
        return caches.match(event.request);
      })
  );
});

5. 资源预加载与优先级控制

预加载关键资源:

<!-- 预加载关键字体 -->
<link rel="preload" href="fonts/important.woff2" as="font" type="font/woff2" crossorigin>

<!-- 预加载关键图片 -->
<link rel="preload" href="images/hero-banner.jpg" as="image">

<!-- 预加载CSS -->
<link rel="preload" href="styles/critical.css" as="style" onload="this.rel='stylesheet'">

<!-- 预加载JavaScript -->
<link rel="preload" href="scripts/critical.js" as="script">

<!-- DNS预连接 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- 预获取下一页资源 -->
<link rel="prefetch" href="next-page.html">
<link rel="prerender" href="https://example.com/next-page">

使用Resource Hints的现代方法:

// 动态添加预加载
function preloadResource(url, as) {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.href = url;
  link.as = as;
  document.head.appendChild(link);
}

// 在需要时预加载资源
if (isUserLikelyToVisitCheckout()) {
  preloadResource('/checkout.css', 'style');
  preloadResource('/checkout.js', 'script');
}

6. 现代打包优化策略

Webpack代码分割示例:

// 动态导入实现路由级代码分割
const ProductPage = lazy(() => import(
  /* webpackChunkName: "product" */ './pages/ProductPage'
));

const CheckoutPage = lazy(() => import(
  /* webpackChunkName: "checkout" */ './pages/CheckoutPage'
));

// 组件级代码分割
const HeavyComponent = lazy(() => import(
  /* webpackPrefetch: true */ './components/HeavyComponent'
));

// Webpack配置优化
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

7. 性能监控与自适应加载

网络状况检测:

// 检测网络状况
function getNetworkInfo() {
  const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
  
  if (connection) {
    return {
      effectiveType: connection.effectiveType, // '4g', '3g', '2g'
      downlink: connection.downlink, // Mbps
      rtt: connection.rtt, // 往返时间
      saveData: connection.saveData // 节流模式
    };
  }
  
  return null;
}

// 根据网络状况调整资源加载
function loadAppropriateResources() {
  const networkInfo = getNetworkInfo();
  
  if (networkInfo && (networkInfo.effectiveType === '2g' || networkInfo.saveData)) {
    // 低速网络:加载轻量版资源
    loadLightVersion();
  } else {
    // 高速网络:加载完整资源
    loadFullVersion();
  }
}

// 图片质量自适应
function getImageQuality() {
  const networkInfo = getNetworkInfo();
  
  if (networkInfo && networkInfo.effectiveType === '2g') {
    return 'low'; // 低质量图片
  } else if (networkInfo && networkInfo.effectiveType === '3g') {
    return 'medium'; // 中等质量
  } else {
    return 'high'; // 高质量
  }
}

8. CDN与边缘计算优化

智能CDN策略:

// 动态选择最近的CDN节点
function getOptimalCDNUrl(resourcePath) {
  const userRegion = getUserRegion(); // 通过IP或浏览器API获取
  const cdnDomains = {
    'na': 'cdn-na.example.com',
    'eu': 'cdn-eu.example.com', 
    'asia': 'cdn-asia.example.com'
  };
  
  const domain = cdnDomains[userRegion] || cdnDomains['na'];
  return `https://${domain}${resourcePath}`;
}

// 使用边缘计算进行A/B测试
async function getPersonalizedContent(userId) {
  // 在CDN边缘节点处理个性化逻辑
  const response = await fetch('/api/personalized', {
    headers: {
      'X-User-ID': userId,
      'X-Edge-Compute': 'true'
    }
  });
  
  return response.json();
}

总结:资源加载优化的核心思想是  "在正确的时间,以正确的优先级,加载正确的资源" 。通过合理的缓存策略、资源预加载、代码分割和网络自适应,可以显著提升应用的加载性能和用户体验。记住,在慢速网络上的性能提升往往比在高速网络上更加明显,也更能体现优化的价值。