【JavaScript 性能优化实战】第五篇:运行时性能优化进阶(懒加载 + 预加载 + 资源优先级)

前端性能优化的核心目标是 "让用户感觉快"------ 首屏秒开、滚动流畅、点击无延迟。而 "运行时性能" 直接决定了用户的实时体验:若首屏加载 10 张非可视区图片,会导致带宽浪费和加载阻塞;若主线程被长时间任务占用,会出现 "点击按钮 2 秒后才响应" 的卡顿。​

本文聚焦浏览器运行时的 4 类核心优化方案,结合 Vue/React 框架实战,从 "原理→代码实现→效果验证" 全流程讲解,帮你把性能指标(如 LCP、TTI)提升至行业优秀水平。​

一、懒加载:"按需加载" 减少首屏负担​

懒加载(Lazy Loading)的核心是 "推迟加载非当前需要的资源"------ 首屏仅加载可视区资源,当用户滚动到非可视区时,再加载对应的图片、组件或脚本。这能显著减少首屏请求数和资源体积,提升 FCP(首次内容绘制)和 LCP(最大内容绘制)。​

1. 图片懒加载:从 "传统监听" 到 "原生 API"​

图片是首屏资源的 "体积大户"(占比通常超 50%),懒加载图片是运行时优化的 "必选项"。​

(1)原生方案:loading="lazy"(最简单,推荐现代项目)​

浏览器原生支持图片懒加载,无需写 JS,只需给<img>或<iframe>添加loading="lazy"属性,浏览器会自动判断 "是否进入可视区" 并加载。​

代码示例:

html 复制代码
<!-- 首屏图片:不懒加载,优先加载 -->
<img src="hero-banner.jpg" alt="首屏banner" width="1200" height="400">

<!-- 非首屏图片:懒加载,进入可视区才加载 -->
<img 
  src="product-1.jpg" 
  alt="商品1" 
  width="300" 
  height="300" 
  loading="lazy"  <!-- 原生懒加载属性 -->
  decoding="async" <!-- 异步解码图片,不阻塞主线程 -->
>

<!-- iframe懒加载(如嵌入的地图、视频) -->
<iframe 
  src="map.html" 
  width="600" 
  height="400" 
  loading="lazy"
></iframe>

**兼容性:**Chrome 77+、Firefox 75+、Edge 79+,覆盖 95% 以上现代浏览器;若需兼容旧浏览器(如 IE),需用 Intersection Observer 降级。​

(2)降级方案:Intersection Observer(精准控制)​

当需要自定义懒加载逻辑(如 "距离可视区 100px 时开始加载")或兼容旧浏览器时,用Intersection Observer(浏览器原生 API,性能优于scroll监听)。​

代码示例(Vue3 组件):

javascript 复制代码
<template>
  <img 
    ref="lazyImg" 
    :data-src="imgSrc"  <!-- 真实地址存data-src,初始不加载 -->
    :alt="imgAlt"
    class="lazy-img"
  >
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  imgSrc: String,
  imgAlt: String
});

const lazyImg = ref(null);
let observer = null;

onMounted(() => {
  // 初始化Intersection Observer
  observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      // 当图片进入可视区
      if (entry.isIntersecting) {
        const img = entry.target;
        // 替换src为真实地址,开始加载
        img.src = img.dataset.src;
        // 加载完成后,停止观察(避免重复触发)
        observer.unobserve(img);
      }
    });
  }, {
    rootMargin: '100px 0px', // 提前100px开始加载(优化体验)
    threshold: 0.1 // 图片10%进入可视区即触发
  });

  // 开始观察当前图片
  if (lazyImg.value) {
    observer.observe(lazyImg.value);
  }
});

onUnmounted(() => {
  // 组件卸载,停止观察(避免内存泄漏)
  if (observer && lazyImg.value) {
    observer.unobserve(lazyImg.value);
  }
});
</script>

<style>
.lazy-img {
  width: 100%;
  height: 200px;
  background: #f5f5f5; /* 占位背景,避免布局抖动 */
}
</style>

**优化效果:**某电商首页(含 20 张商品图),未懒加载时首屏资源体积 2.1MB,加载时间 3.8 秒;懒加载后首屏仅加载 3 张图,体积 420KB,加载时间 1.2 秒,LCP 从 3.2 秒降至 1.1 秒。​

2. 组件与路由懒加载:避免 "一次性加载所有代码"​

路由懒加载在之前的代码分割中提过,此处深化 "组件级懒加载"------ 对 "非首屏且非立即渲染" 的组件(如弹窗、tab 页内容),仅在需要时加载,减少初始 JS 体积。​

(1)Vue3 组件懒加载:defineAsyncComponent​

Vue3 提供defineAsyncComponentAPI,支持动态加载组件,配合Suspense实现加载状态管理。​

代码示例:

javascript 复制代码
<template>
  <div>
    <button @click="showModal = true">打开弹窗</button>
    <!-- Suspense:包裹懒加载组件,处理加载/错误状态 -->
    <Suspense v-if="showModal">
      <template #default>
        <!-- 懒加载的弹窗组件 -->
        <LazyModal @close="showModal = false" />
      </template>
      <template #fallback>
        <!-- 加载中状态 -->
        <div class="loading">加载中...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue';

// 1. 懒加载组件:仅当组件被渲染时,才加载对应的JS chunk
const LazyModal = defineAsyncComponent(() => 
  import('./components/LazyModal.vue') // 打包时会拆分为LazyModal.[hash].js
);

const showModal = ref(false);
</script>
(2)React 组件懒加载:React.lazy + Suspense​

React 通过React.lazy和Suspense实现组件懒加载,逻辑与 Vue 类似。​

代码示例:

javascript 复制代码
import { useState, Suspense, lazy } from 'react';

// 懒加载组件:仅在需要时加载
const LazyTabContent = lazy(() => import('./LazyTabContent'));

function TabComponent() {
  const [activeTab, setActiveTab] = useState('tab1');

  return (
    <div>
      <div className="tabs">
        <button onClick={() => setActiveTab('tab1')}>标签1</button>
        <button onClick={() => setActiveTab('tab2')}>标签2(懒加载)</button>
      </div>
      
      {activeTab === 'tab1' ? (
        <div>标签1的即时内容</div>
      ) : (
        // Suspense:处理懒加载组件的加载状态
        <Suspense fallback={<div>加载中...</div>}>
          <LazyTabContent />
        </Suspense>
      )}
    </div>
  );
}

优化效果:某管理系统,未懒加载时初始 JS 体积 1.8MB,加载时间 2.5 秒;组件懒加载后初始 JS 体积降至 900KB,加载时间 1.3 秒,TTI(可交互时间)从 3.8 秒降至 2.1 秒。​

二、预加载与预连接:"提前准备" 提升后续体验​

预加载(Preload)和预连接(Preconnect)是 "主动提前获取资源" 的优化方案 ------ 通过预判用户行为(如下一页跳转、点击操作),提前加载资源或建立连接,减少后续操作的等待时间。​

很多开发者混淆preload、prefetch、preconnect、dns-prefetch,需先明确各自的用途:​

|---------------|---------------------------------|---------------------------------------|------|
| rel 属性​ | 作用​ | 适用场景​ | 优先级​ |
| preload​ | 提前加载 "当前页面马上要用" 的资源​ | 关键 CSS、首屏 JS、核心图片​ | 高​ |
| prefetch​ | 提前加载 "下一页可能用" 的资源​ | 下一页的路由 JS、非首屏图片​ | 低​ |
| preconnect​ | 提前建立 "与目标域名" 的 TCP 连接 + SSL 握手​ | CDN 域名、API 域名​ | 中​ |
| dns-prefetch​ | 提前解析 "目标域名" 的 DNS(仅 DNS 查询)​ | 兼容旧浏览器(如 Chrome < 46),替代 preconnect​ | 低​ |

2. 实战:预加载关键资源​

(1)preload:加载当前页面关键资源​

例如,首屏 JS 体积大,需提前加载避免阻塞渲染;或关键 CSS 需优先加载,避免 "无样式内容闪烁(FOUC)"。​

代码示例:

html 复制代码
<!-- 1. 预加载关键JS(as指定资源类型,确保浏览器正确处理) -->
<link 
  rel="preload" 
  href="/js/main.[hash].js" 
  as="script"  <!-- as必须正确:script/js、style/css、image/png等 -->
  crossorigin="anonymous"  <!-- 跨域资源需加,否则预加载无效 -->
>

<!-- 2. 预加载关键CSS(避免FOUC) -->
<link 
  rel="preload" 
  href="/css/critical.[hash].css" 
  as="style"
>
<!-- 预加载完成后,需手动关联到页面 -->
<link rel="stylesheet" href="/css/critical.[hash].css">

<!-- 3. 预加载首屏大图(避免LCP延迟) -->
<link 
  rel="preload" 
  href="/img/hero-banner.[hash].webp" 
  as="image"
  imagesrcset="/img/hero-banner-480.[hash].webp 480w, /img/hero-banner-1200.[hash].webp 1200w"
  imagesizes="100vw"
>

**注意:**避免滥用 preload------ 若预加载非关键资源,会抢占带宽,导致真正关键的资源加载延迟。​

(2)prefetch:加载下一页资源​

例如,用户当前在首页,预判其可能点击 "商品列表" 进入下一页,提前加载商品列表页的路由 JS。​

代码示例:

html 复制代码
<!-- 预加载下一页(商品列表页)的路由JS -->
<link 
  rel="prefetch" 
  href="/js/pages/goods-list.[hash].js" 
  as="script"
>

<!-- 预加载下一页可能用到的图标字体 -->
<link 
  rel="prefetch" 
  href="/fonts/iconfont.[hash].woff2" 
  as="font"
  type="font/woff2"
  crossorigin="anonymous"
>

**优化效果:**某博客网站,首页预加载 "文章详情页" 的 JS,用户点击进入详情页时,加载时间从 1.5 秒缩短至 0.3 秒,跳转无感知。​

3. 预连接:减少连接建立时间​

浏览器与服务器建立连接需经过 "DNS 解析→TCP 三次握手→SSL 四次握手"(HTTPS 场景),总耗时约 100-500ms。通过preconnect提前建立连接,可省去这部分时间。​

代码示例:

html 复制代码
<!-- 1. 预连接CDN域名(提前建立连接,后续加载CDN资源时无需握手) -->
<link rel="preconnect" href="https://cdn.example.com">

<!-- 2. 预连接API域名(提前建立连接,后续接口请求时更快) -->
<link rel="preconnect" href="https://api.example.com">

<!-- 3. 兼容旧浏览器:DNS预解析(仅解析DNS,不建立TCP/SSL连接) -->
<link rel="dns-prefetch" href="https://cdn.example.com">

优化效果:某项目通过预连接 CDN 域名,首次加载 CDN 资源的时间从 350ms 缩短至 120ms,减少 65% 的连接耗时。​

三、资源优先级控制:让 "关键资源" 先加载​

浏览器会默认分配资源优先级(如 JS>CSS > 图片),但有时默认策略不符合业务需求(如某非首屏 JS 阻塞了关键 CSS 加载),需手动调整优先级。​

1. JS 加载优先级:defer vs async​

JS 默认会 "阻塞 HTML 解析和 CSS 渲染",通过defer和async可改变 JS 的执行时机,避免阻塞关键资源。​

|--------|--------------------------|----------|------------------|
| 属性​ | 执行时机​ | 顺序性​ | 适用场景​ |
| 无​ | 立即执行,阻塞 HTML 解析和 CSS 渲染​ | 按引入顺序执行​ | 首屏关键 JS(如初始化代码)​ |
| defer​ | HTML 解析完成后执行,不阻塞解析​ | 按引入顺序执行​ | 非首屏 JS(如统计脚本)​ |
| async​ | 下载完成后立即执行,不阻塞解析但可能阻塞渲染​ | 不保证顺序​ | 独立脚本(如广告脚本)​ |

代码示例:

html 复制代码
<!-- 1. 首屏关键JS:无属性,立即执行(确保优先初始化) -->
<script src="/js/critical-init.js"></script>

<!-- 2. 非首屏JS:defer,HTML解析完执行,保持顺序 -->
<script src="/js/analytics.js" defer></script>
<script src="/js/track.js" defer></script> <!-- 会在analytics.js之后执行 -->

<!-- 3. 独立脚本:async,下载完立即执行,不保证顺序 -->
<script src="/js/ad-script.js" async></script>

2. CSS 加载优先级:内联关键 CSS,异步加载非关键 CSS​

CSS 会 "阻塞 HTML 解析和页面渲染"(避免 FOUC),但非关键 CSS(如页脚、非首屏组件的 CSS)无需优先加载,可异步加载。​

优化方案:​

  1. 内联关键 CSS(首屏所需的 CSS)到<head>,减少请求数;
  2. 异步加载非关键 CSS,避免阻塞渲染。

代码示例:

html 复制代码
<head>
  <!-- 1. 内联关键CSS(首屏样式,约1-2KB) -->
  <style>
    .header { height: 60px; background: #fff; }
    .hero-banner { width: 100%; height: 400px; }
    /* 仅包含首屏必需的样式 */
  </style>

  <!-- 2. 异步加载非关键CSS(如页脚、商品列表样式) -->
  <link 
    rel="preload" 
    href="/css/non-critical.[hash].css" 
    as="style"
    onload="this.onload=null; this.rel='stylesheet'"  <!-- 加载完成后关联为CSS -->
  >
  <!-- 兼容旧浏览器:noscript降级 -->
  <noscript>
    <link rel="stylesheet" href="/css/non-critical.[hash].css">
  </noscript>
</head>

**优化效果:**某官网,未拆分 CSS 时首屏需加载 15KB 的 CSS(1 个请求),加载时间 300ms;拆分后内联 3KB 关键 CSS(无请求),异步加载 12KB 非关键 CSS,首屏渲染时间从 800ms 缩短至 450ms。​

四、主线程阻塞优化:避免 "卡顿" 交互​

浏览器的主线程负责 "JS 执行、HTML 解析、CSS 渲染、事件处理",若主线程被长时间任务(如 100ms 以上的 JS 计算)占用,会导致 "点击无响应""滚动掉帧"。​

1. 识别长时间任务:Chrome DevTools Performance​

打开 Chrome DevTools → Performance 标签 → 录制 3-5 秒操作 → 查看 "Main" 线程的 "Long Tasks"(红色块,超过 50ms 的任务),定位耗时函数。​

2. 优化方案:拆分任务 + Web Workers​

(1)拆分长时间任务:用 requestIdleCallback​

将耗时任务拆分为多个小任务,在主线程空闲时执行,不阻塞 UI。​

**代码示例:**处理 10 万条数据(非实时需求)

javascript 复制代码
// 原始方案:一次性处理10万条数据,阻塞主线程200ms
function processLargeData(rawData) {
  const result = [];
  rawData.forEach(item => {
    // 复杂计算:过滤、格式化数据
    if (item.value > 100) {
      result.push({ id: item.id, value: item.value * 2 });
    }
  });
  return result;
}

// 优化方案:用requestIdleCallback拆分任务
function processLargeDataAsync(rawData, callback) {
  const result = [];
  const total = rawData.length;
  let index = 0;

  // 每次处理100条数据(小任务)
  function processBatch(deadline) {
    // 主线程空闲且未处理完
    while (index < total && deadline.timeRemaining() > 0) {
      const item = rawData[index];
      if (item.value > 100) {
        result.push({ id: item.id, value: item.value * 2 });
      }
      index++;
    }

    // 处理完所有数据,执行回调
    if (index >= total) {
      callback(result);
    } else {
      // 主线程繁忙,下次空闲再处理
      requestIdleCallback(processBatch);
    }
  }

  // 启动拆分任务
  requestIdleCallback(processBatch);
}

// 调用:处理完后更新UI
processLargeDataAsync(largeRawData, (processedData) => {
  console.log('处理完成', processedData);
  // 仅在数据处理完后更新UI,不阻塞主线程
  updateUI(processedData);
});
(2)Heavy Task 移至 Web Workers​

对于 "实时需求且耗时超 100ms" 的任务(如大数据可视化计算、Excel 解析),用 Web Workers 在后台线程处理,完全不阻塞主线程。​

代码示例(Vue3):

javascript 复制代码
<template>
  <div>
    <button @click="startCalculation">开始大数据计算</button>
    <div v-if="loading">计算中...</div>
    <div v-if="result">计算结果:{{ result }}</div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const loading = ref(false);
const result = ref(null);
let worker = null;

// 初始化Web Worker(单独的JS文件)
function initWorker() {
  // 浏览器支持Web Workers时创建
  if (window.Worker) {
    // worker.js是后台线程的代码文件
    worker = new Worker(new URL('./worker.js', import.meta.url));
    
    // 接收后台线程的消息(计算结果)
    worker.onmessage = (e) => {
      loading.value = false;
      result.value = e.data; // e.data是worker发送的结果
    };
    
    // 处理worker错误
    worker.onerror = (error) => {
      loading.value = false;
      console.error('Worker错误:', error);
    };
  } else {
    alert('浏览器不支持Web Workers');
  }
}

// 开始计算(主线程发送消息给worker)
const startCalculation = () => {
  if (!worker) initWorker();
  loading.value = true;
  result.value = null;
  
  // 发送需要计算的大数据(主线程→worker)
  worker.postMessage({ 
    type: 'calculate', 
    data: new Array(100000).fill(Math.random() * 1000) // 10万条随机数
  });
};

// 组件卸载,终止worker(避免内存泄漏)
onUnmounted(() => {
  if (worker) {
    worker.terminate();
    worker = null;
  }
});
</script>

worker.js(后台线程代码):

javascript 复制代码
// 接收主线程的消息
self.onmessage = (e) => {
  if (e.data.type === 'calculate') {
    const { data } = e.data;
    // 耗时计算:求所有数的平均值(约150ms,在后台线程执行)
    const sum = data.reduce((acc, curr) => acc + curr, 0);
    const avg = sum / data.length;
    
    // 发送计算结果给主线程
    self.postMessage(avg.toFixed(2));
  }
};

优化效果:大数据计算任务从 "阻塞主线程 150ms(导致点击无响应)" 变为 "后台线程执行,主线程完全流畅",交互响应时间从 180ms 降至 20ms。​

五、总结与后续预告​

本文围绕 "运行时性能" 展开,核心优化思路可总结为 3 点:​

  1. 按需加载:用懒加载推迟非关键资源,减少首屏负担;
  2. 提前准备:用预加载 / 预连接预判资源需求,缩短后续等待时间;
  3. 优先级管控:让关键资源先加载,避免主线程被阻塞。

某真实项目优化前后的核心性能指标对比:​

|--------------|--------|--------|-------|
| 性能指标​ | 优化前​ | 优化后​ | 提升幅度​ |
| 首次内容绘制(FCP)​ | 1.8 秒​ | 0.9 秒​ | 50%​ |
| 最大内容绘制(LCP)​ | 3.2 秒​ | 1.1 秒​ | 65%​ |
| 可交互时间(TTI)​ | 4.5 秒​ | 2.0 秒​ | 55%​ |
| 主线程阻塞时间​ | 320ms​ | 45ms​ | 86%​ |

下一篇文章,我们将聚焦 "性能监控与自动化优化",讲解如何用 Lighthouse、Core Web Vitals 等工具量化性能,以及如何在 CI/CD 流程中集成性能检测,实现 "性能问题自动预警",形成优化闭环。

相关推荐
1024小神2 小时前
flutter 使用dio发送本地https请求报错
前端
正义的大古2 小时前
OpenLayers地图交互 -- 章节七:指针交互详解
前端·javascript·vue.js·openlayers
小中12342 小时前
文件导出的几种方式
前端
qwy7152292581632 小时前
vue自定义指令
前端·javascript·vue.js
niusir2 小时前
Zustand 实战:10 行代码搞定全局状态
前端·javascript·react.js
niusir2 小时前
React 状态管理的演进与最佳实践
前端·javascript·react.js
张愚歌2 小时前
快速上手Leaflet:轻松创建你的第一个交互地图
前端
唐某人丶2 小时前
教你如何用 JS 实现 Agent 系统(3)—— 借鉴 Cursor 的设计模式实现深度搜索
前端·人工智能·aigc
看到我请叫我铁锤2 小时前
vue3使用leaflet的时候高亮显示省市区
前端·javascript·vue.js