资源加载时机的精细控制

说实话,我在处理一个电商项目时发现,页面加载速度慢得让人无法忍受。经过排查,问题出在资源加载策略上。很多人只知道要压缩资源,却忽略了加载时机的控制。

预加载与懒加载的平衡艺术

这里有个细节:预加载(preload)和懒加载(lazy loading)并不是对立的技术,而是需要根据资源类型进行组合使用。根据HTTP Archive的数据统计,2023年移动端页面中,图片资源平均占页面总大小的60%以上。

// 关键资源的预加载
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.href = 'critical-font.woff2';
preloadLink.as = 'font';
preloadLink.type = 'font/woff2';
preloadLink.crossOrigin = 'anonymous';
document.head.appendChild(preloadLink);

// 非关键图片的懒加载
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      imageObserver.unobserve(img);
    }
  });
});

lazyImages.forEach(img => imageObserver.observe(img));

实测结论是:将首屏关键资源预加载,非首屏图片懒加载,可以让LCP(Largest Contentful Paint)指标提升40%以上。

JavaScript执行性能的深层优化

微观任务拆分的威力

不知道你有没有遇到过这样的情况:页面交互卡顿,但CPU使用率并不高?问题往往出在JavaScript的任务执行策略上。

现代浏览器基于事件循环机制,长时间运行的JavaScript任务会阻塞UI渲染。根据Google的研究,当任务执行时间超过50ms时,用户就能明显感知到卡顿。

// 糟糕的长任务示例
function processLargeData(data) {
  // 这个函数可能执行超过100ms
  return data.map(item => {
    // 复杂的计算逻辑
    return heavyComputation(item);
  });
}

// 优化后的版本
async function processLargeDataOptimized(data) {
  const results = [];
  const chunkSize = 100;
  
  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);
    
    // 使用setTimeout拆分成多个宏任务
    await new Promise(resolve => {
      setTimeout(() => {
        results.push(...chunk.map(item => heavyComputation(item)));
        resolve();
      }, 0);
    });
  }
  
  return results;
}

这个技巧在处理大数据量时特别有效。我在处理一个数据可视化项目时,通过任务拆分将交互响应时间从300ms降到了50ms以内。

CSS性能的隐藏杀手

重排与重绘的触发条件

说真的,CSS性能问题往往比JavaScript更难排查。特别是重排(reflow)和重绘(repaint),它们对性能的影响是累积性的。

根据Chrome DevTools的官方文档,以下属性修改会触发重排:

  • width, height, margin, padding
  • display, position, float
  • font-size, font-family
  • 获取offsetTop、scrollTop等布局信息

而以下属性只触发重绘:

  • color, background-color, visibility
  • border-radius, box-shadow
  • outline, text-decoration
/* 避免在动画中触发重排 */
.bad-animation {
  transition: all 0.3s;
  /* left变化会触发重排 */
  left: 0px;
}

.good-animation {
  transition: transform 0.3s;
  /* transform不会触发重排 */
  transform: translateX(0);
}

有个项目让我印象深刻:一个看似简单的hover动画导致页面严重卡顿。最后发现是频繁修改width属性触发了连锁重排。改用transform后性能立即提升了3倍。

内存泄漏的检测与预防

现代前端框架中的内存管理

你以为用了React、Vue就不用关心内存管理了?实测结论是:框架只能帮你避免一部分问题,真正的内存管理还需要开发者主动参与。

常见的内存泄漏场景:

  • 未清理的事件监听器
  • 未取消的定时器
  • DOM引用未释放
  • 闭包中的变量引用
// React组件中的内存泄漏示例
function ProblematicComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const timer = setInterval(() => {
      fetchData().then(setData);
    }, 1000);
    
    // 忘记清理定时器!
    // return () => clearInterval(timer);
  }, []);
  
  return <div>{data}</div>;
}

// 正确的写法
function FixedComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    const timer = setInterval(() => {
      if (isMounted) {
        fetchData().then(setData);
      }
    }, 1000);
    
    return () => {
      isMounted = false;
      clearInterval(timer);
    };
  }, []);
  
  return <div>{data}</div>;
}

使用Chrome DevTools的Memory面板定期检查内存使用情况是个好习惯。我在一个单页应用中曾经发现内存使用量每小时增加50MB,最终定位到一个第三方库未正确清理WebSocket连接。

构建工具的优化配置

Webpack与Vite的配置陷阱

构建工具的配置差异对性能影响巨大。以Webpack为例,不同版本的配置语法和优化策略都有所不同。

Webpack 5引入了持久化缓存,但默认是不开启的:

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem', // 使用文件系统缓存
    buildDependencies: {
      config: [__filename], // 当webpack配置变化时失效缓存
    },
  },
  optimization: {
    moduleIds: 'deterministic', // 保持模块ID稳定
    chunkIds: 'deterministic', // 保持chunkID稳定
  },
};

而对于Vite,虽然开发环境很快,但生产构建仍然需要优化:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'dayjs']
        }
      }
    }
  }
}

这里有个细节:Vite 4.x版本在代码分割策略上比3.x更加智能,但手动配置chunks仍然能带来更好的缓存命中率。

网络请求的优化策略

HTTP/2与资源合并的悖论

很多人还在沿用HTTP/1.1时代的优化策略,在HTTP/2环境下反而会适得其反。根据Akamai的统计数据,使用HTTP/2的网站中,过度资源合并反而会导致性能下降15%。

HTTP/2的多路复用特性使得多个小文件的并行加载效率更高。在实践中,我建议:

  • 将CSS拆分为关键CSS和非关键CSS
  • JavaScript按路由拆分,而不是全部打包
  • 图片使用WebP格式,并设置合适的压缩质量
  • 启用Brotli压缩,比Gzip效率高15-20%

这些优化技巧都是我在实际项目中踩过坑后总结出来的。性能优化没有银弹,关键是要根据具体的业务场景和用户设备特点来制定策略。记住,最快的请求是不发请求,最快的渲染是不做渲染!