JavaScript 性能优化系列(二):首屏加载加速——从「等待」到「瞬间呈现」的突破

引言:首屏加载为何是用户体验的「第一道门槛」?

在前端性能优化领域,有一个被反复验证的真理:用户对等待的耐心是有限的。研究数据显示,当首屏加载时间超过3秒,53%的移动用户会放弃访问;每减少1秒加载时间,可提升7%的转化率。首屏加载速度直接决定了用户对产品的第一印象,是用户体验的「第一道门槛」。

首屏加载(First Contentful Paint, FCP)指的是从用户输入URL到页面首次呈现任何内容(文本、图像、SVG等)的时间。它与 Largest Contentful Paint (LCP) 共同构成了Core Web Vitals的核心指标,直接影响搜索引擎排名和用户留存率。

与包体积优化不同,首屏加载加速不仅关注「文件大小」,更关注「加载顺序」、「资源优先级」和「渲染效率」。本篇作为JavaScript性能优化系列的第二篇,将深入探讨四大核心策略:关键资源优先加载、预加载与预连接、服务端优化、缓存策略,帮助你将首屏加载时间从「秒级」压缩到「毫秒级」。

2.1 关键资源优先加载:让「核心内容」先出现

关键资源指的是首屏渲染所必需的资源,包括HTML结构、核心CSS、关键JavaScript和首屏图片。关键资源优先加载的核心思想是:识别并优先传输首屏必需的资源,延迟或异步加载非必需资源,从而最小化首屏渲染时间。

2.1.1 原理:关键资源的「渲染阻塞特性」

浏览器渲染页面的流程可简化为:

  1. HTML解析:将HTML解析为DOM树;
  2. CSS解析:将CSS解析为CSSOM树;
  3. 渲染树构建:结合DOM和CSSOM生成渲染树;
  4. 布局(Layout):计算元素的位置和大小;
  5. 绘制(Paint):将元素绘制到屏幕上;
  6. 合成(Composite):将绘制的图层合并为最终画面。

其中,HTML和CSS是阻塞渲染的资源

  • HTML阻塞DOM构建,没有DOM就无法进行后续渲染;
  • CSS阻塞CSSOM构建,而渲染树需要等待CSSOM和DOM都准备好才能构建;
  • JavaScript既可能阻塞HTML解析(默认),也可能阻塞CSSOM(如果JS操作CSS)。

关键资源优先加载的本质是减少「渲染阻塞资源的数量和体积」,并优先传输这些资源

2.1.2 代码样例:关键资源识别与优先加载实践

2.1.2.1 识别关键资源(Lighthouse工具使用)

Lighthouse是Chrome提供的性能分析工具,可自动识别关键资源:

  1. 在Chrome中打开页面,按F12打开开发者工具;
  2. 切换到Lighthouse标签,勾选「Performance」;
  3. 点击「Generate report」,等待分析完成;
  4. 在报告的「Opportunities」部分查看「Remove unused JavaScript」和「Remove unused CSS」,识别非关键资源。
2.1.2.2 内联关键CSS(避免额外请求)

将首屏必需的CSS内联到HTML的<style>标签中,避免额外的HTTP请求阻塞渲染:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>首屏优化示例</title>
  <!-- 内联关键CSS(仅包含首屏必需样式) -->
  <style>
    /* 符合谷歌CSS规范:使用kebab-case类名,避免嵌套过深 */
    .header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 16px;
      background-color: #fff;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    
    .hero-banner {
      width: 100%;
      height: 200px;
      background-image: url('hero-banner.webp');
      background-size: cover;
      background-position: center;
    }
    
    .main-nav {
      display: flex;
      gap: 24px;
    }
    
    .nav-link {
      color: #333;
      text-decoration: none;
      font-size: 16px;
    }
  </style>
  <!-- 非关键CSS异步加载(不阻塞渲染) -->
  <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>
  <header class="header">
    <div class="logo">品牌Logo</div>
    <nav class="main-nav">
      <a href="/" class="nav-link">首页</a>
      <a href="/products" class="nav-link">产品</a>
      <a href="/about" class="nav-link">关于我们</a>
    </nav>
  </header>
  <div class="hero-banner"></div>
  <!-- 页面内容... -->
</body>
</html>
2.1.2.3 异步加载非关键JavaScript

使用asyncdefer属性异步加载非首屏必需的JavaScript,避免阻塞HTML解析:

<!-- 1. 关键JS(首屏交互必需):同步加载但放在<body>底部 -->
<script src="critical.js"></script>

<!-- 2. 非关键JS(如统计、广告):使用async异步加载 -->
<script src="analytics.js" async></script>

<!-- 3. 依赖DOM的非关键JS:使用defer(按顺序执行,DOM加载完成后执行) -->
<script src="carousel.js" defer></script>
<script src="tooltip.js" defer></script> <!-- 会在carousel.js之后执行 -->

<!-- 4. 现代浏览器:使用module类型默认异步加载 -->
<script type="module" src="feature-module.js"></script>
<!-- 兼容旧浏览器 -->
<script nomodule src="feature-fallback.js" defer></script>

asyncdefer的区别:

  • async:下载完成后立即执行(执行顺序不确定);
  • defer:下载完成后等待DOM解析完成,按引入顺序执行。
2.1.2.4 首屏图片优化(优先加载LCP元素)

Largest Contentful Paint (LCP) 元素通常是首屏的主要图片或文本块,优化LCP元素可显著提升首屏体验:

<!-- 1. LCP图片:使用preload优先加载 -->
<link rel="preload" href="hero-banner.webp" as="image" fetchpriority="high">

<!-- 2. 正确设置图片尺寸,避免布局偏移 -->
<img 
  src="hero-banner.webp" 
  alt="首页横幅" 
  width="1200" 
  height="400" 
  class="hero-banner"
  loading="eager" <!-- 首屏图片禁用懒加载 -->
>

<!-- 3. 使用响应式图片,根据设备加载合适尺寸 -->
<picture>
  <source 
    srcset="hero-banner-large.webp" 
    media="(min-width: 1024px)"
  >
  <source 
    srcset="hero-banner-medium.webp" 
    media="(min-width: 768px)"
  >
  <img 
    src="hero-banner-small.webp" 
    alt="首页横幅" 
    width="1200" 
    height="400"
  >
</picture>
2.1.2.5 减少关键资源数量(合并与精简)
// 错误:多个小体积关键JS文件(增加请求数)
<script src="utils.js"></script>
<script src="api.js"></script>
<script src="auth.js"></script>

// 正确:合并为一个关键JS文件(减少请求数)
<script src="critical-bundle.js"></script>

使用Webpack合并关键资源:

// webpack.config.js
module.exports = {
  entry: {
    critical: './src/critical.js', // 关键JS入口
    app: './src/app.js', // 非关键JS入口
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        critical: {
          name: 'critical',
          test: /critical/,
          chunks: 'initial',
          priority: 10, // 优先处理
          enforce: true,
        },
      },
    },
  },
};

2.1.3 实践反例:这些做法会「阻塞」首屏加载

2.1.3.1 反例1:在<head>中加载非关键JS(阻塞HTML解析)
<!-- 错误:在<head>中加载非关键JS,阻塞HTML解析 -->
<head>
  <script src="carousel.js"></script> <!-- 非首屏必需,却阻塞解析 -->
  <script src="chart.js"></script> <!-- 非首屏必需 -->
</head>

问题:浏览器在解析HTML时遇到<script>标签会暂停解析,等待JS下载并执行完成,导致首屏内容无法及时呈现。

2.1.3.2 反例2:加载未使用的CSS(增加关键资源体积)
/* 错误:关键CSS中包含非首屏样式(增加体积) */
/* 首屏不需要的样式 */
.footer {
  background-color: #f5f5f5;
  padding: 20px;
}

.product-detail {
  /* 产品详情页样式,首屏不需要 */
}

问题:关键CSS体积过大会增加下载时间,延缓CSSOM构建,进而阻塞渲染。

2.1.3.3 反例3:首屏图片使用懒加载
<!-- 错误:首屏LCP图片使用懒加载 -->
<img 
  src="hero-banner.webp" 
  alt="首页横幅" 
  loading="lazy" <!-- 首屏图片不应该懒加载 -->
>

问题:懒加载会延迟首屏图片的加载,导致LCP时间变长,首屏出现空白区域。

2.1.3.4 反例4:关键资源未设置优先级
<!-- 错误:关键资源和非关键资源优先级相同 -->
<link rel="stylesheet" href="critical.css">
<link rel="stylesheet" href="non-critical.css">

<script src="critical.js"></script>
<script src="non-critical.js"></script>

问题:浏览器可能会先加载非关键资源,导致关键资源加载延迟,延长首屏渲染时间。

2.1.4 代码评审要点:关键资源优先加载的「6个检查项」

评审维度检查要点工具支持
关键资源识别1. 是否明确区分关键资源和非关键资源?
2. 关键资源是否仅包含首屏必需内容?
1. Lighthouse报告
2. Chrome DevTools → Coverage面板
CSS处理1. 关键CSS是否内联到HTML中?
2. 非关键CSS是否异步加载?
3. 是否移除了关键CSS中的未使用样式?
1. 查看HTML源码中的<style>标签
2. 使用PurgeCSS检测未使用样式
JS处理1. 非关键JS是否使用async/defer/module异步加载?
2. 关键JS是否放在<body>底部?
3. 是否避免在首屏JS中执行耗时操作?
1. 检查<script>标签的async/defer属性
2. Chrome DevTools → Performance面板
图片处理1. LCP图片是否优先加载?
2. 首屏图片是否禁用懒加载?
3. 是否使用合适尺寸的图片?
1. Lighthouse的LCP指标
2. 检查图片的loading属性和尺寸属性
资源数量1. 关键资源数量是否控制在5个以内?
2. 是否合并了小体积的关键资源?
1. Chrome DevTools → Network面板
2. Webpack Bundle Analyzer
加载顺序1. HTML是否优先于CSS和JS加载?
2. CSS是否优先于非关键JS加载?
1. Chrome DevTools → Network面板的瀑布图
2. 检查资源的加载顺序

2.2 预加载与预连接:提前「预判」用户需求

预加载(Preload)、预连接(Preconnect)和预获取(Prefetch)是浏览器提供的资源加载优化技术,通过「预判用户行为」提前加载资源,减少用户操作时的等待时间。这些技术不会阻塞页面的初始渲染,但能显著提升后续交互的响应速度。

2.2.1 原理:预加载技术的「工作机制」

浏览器默认的资源加载顺序基于HTML解析顺序,而预加载技术允许开发者「主动干预」资源加载优先级和时机:

技术语法作用适用场景
Preload<link rel="preload">强制浏览器提前加载指定资源,优先级高首屏即将用到的关键资源(如字体、LCP图片)
Prefetch<link rel="prefetch">浏览器空闲时加载资源,优先级低用户可能在后续页面用到的资源(如跳转目标页的JS/CSS)
Preconnect<link rel="preconnect">提前建立与第三方域名的连接(DNS+TCP+TLS)即将从第三方域名加载资源(如CDN、广告、统计)
Dns-prefetch<link rel="dns-prefetch">提前解析域名的DNS预connect的轻量版,仅做DNS解析
Prerender<link rel="prerender">提前渲染整个页面到后台高度确定用户会访问的页面(如搜索结果页)

这些技术的核心价值在于减少资源加载的「延迟时间」

  • Preload解决「关键资源加载晚」的问题;
  • Prefetch解决「后续资源加载慢」的问题;
  • Preconnect解决「跨域资源连接耗时」的问题(DNS解析通常需要20-120ms,TCP握手需要40-300ms)。

2.2.2 代码样例:预加载技术的正确使用

2.2.2.1 Preload:优先加载首屏关键资源
<head>
  <!-- 1. 预加载首屏字体(字体加载会阻塞文本渲染) -->
  <link 
    rel="preload" 
    href="main-font.woff2" 
    as="font" 
    type="font/woff2" 
    crossorigin 
    fetchpriority="high"
  >
  
  <!-- 2. 预加载LCP图片 -->
  <link 
    rel="preload" 
    href="hero-banner.webp" 
    as="image" 
    fetchpriority="high"
  >
  
  <!-- 3. 预加载关键JS(加载但不执行,需要手动执行) -->
  <link 
    rel="preload" 
    href="critical-component.js" 
    as="script" 
    onload="const script = document.createElement('script'); script.src = this.href; document.body.appendChild(script);"
  >
  
  <!-- 4. 预加载CSS(加载后手动应用) -->
  <link 
    rel="preload" 
    href="theme.css" 
    as="style" 
    onload="this.rel = 'stylesheet'"
  >
  <noscript><link rel="stylesheet" href="theme.css"></noscript>
</head>

注意:preload只是提前加载资源,不会自动执行或应用,需要配合onload处理。

2.2.2.2 Prefetch:预加载可能用到的资源
<!-- 1. 预加载下一页可能用到的JS -->
<link rel="prefetch" href="product-list.js" as="script">

<!-- 2. 预加载用户可能点击的图片 -->
<link rel="prefetch" href="product-detail-image.webp" as="image">

<!-- 3. 在首页预加载购物车页面的资源 -->
<link rel="prefetch" href="cart.css" as="style">
<link rel="prefetch" href="cart.js" as="script">

在单页应用中,可根据路由预测预加载资源:

// 路由切换时预加载可能访问的路由资源(Vue Router示例)
router.afterEach((to, from) => {
  // 分析用户行为,预测可能访问的下一个路由
  const likelyNextRoutes = getLikelyNextRoutes(to.path);
  
  likelyNextRoutes.forEach(routePath => {
    // 根据路由路径获取对应的组件Chunk
    const chunkName = getChunkNameByRoute(routePath);
    if (chunkName && !isPrefetched(chunkName)) {
      // 动态导入实现prefetch效果
      import(/* webpackPrefetch: true */ `../views/${chunkName}.vue`);
      markAsPrefetched(chunkName);
    }
  });
});
2.2.2.3 Preconnect与Dns-prefetch:优化跨域资源加载
<!-- 1. 预连接到CDN域名(建立DNS+TCP+TLS连接) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

<!-- 2. 预连接到广告域名 -->
<link rel="preconnect" href="https://ads.example.net">

<!-- 3. 对可能需要的域名进行DNS预解析 -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<link rel="dns-prefetch" href="https://video.example.org">

最佳实践:对于一定会用到的跨域资源用preconnect,对于可能用到的用dns-prefetch。

2.2.2.4 结合用户行为的智能预加载

根据用户交互(如鼠标悬停、滚动)预测并预加载资源:

// 当用户悬停在导航链接上时,预加载目标页面资源
document.querySelectorAll('.nav-link').forEach(link => {
  link.addEventListener('mouseenter', () => {
    const targetUrl = link.getAttribute('href');
    const prefetchUrl = getResourceUrlByRoute(targetUrl);
    
    if (prefetchUrl && !isAlreadyLoading(prefetchUrl)) {
      // 创建link标签进行prefetch
      const prefetchLink = document.createElement('link');
      prefetchLink.rel = 'prefetch';
      prefetchLink.href = prefetchUrl;
      prefetchLink.as = 'script';
      document.head.appendChild(prefetchLink);
      
      // 3秒后移除未使用的prefetch(避免浪费带宽)
      setTimeout(() => {
        if (document.head.contains(prefetchLink)) {
          document.head.removeChild(prefetchLink);
        }
      }, 3000);
    }
  });
});

2.2.3 实践反例:预加载技术的「滥用与误用」

2.2.3.1 反例1:过度使用Preload导致资源竞争
<!-- 错误:过度使用preload,导致关键资源带宽被抢占 -->
<link rel="preload" href="image1.webp" as="image">
<link rel="preload" href="image2.webp" as="image">

问题:浏览器的并发连接数有限(通常6个/域名),过度使用preload会导致关键资源(如CSS、HTML)加载延迟,反而影响首屏渲染。

2.2.3.2 反例2:Prefetch不必要的资源浪费带宽
<!-- 错误:prefetch大量用户可能不会访问的资源 -->
<link rel="prefetch" href="about-us.js">
<link rel="prefetch" href="contact.js">

问题:移动用户通常带宽有限,prefetch不必要的资源会浪费用户流量,甚至可能因占用带宽影响当前页面的加载。

2.2.3.3 反例3:Preload忘记设置as属性
<!-- 错误:preload未设置as属性,浏览器无法正确处理资源 -->
<link rel="preload" href="main-font.woff2">

问题:缺少as属性会导致浏览器将资源视为「未知类型」,无法正确设置优先级和处理方式,preload失效。

2.2.3.4 反例4:对所有跨域域名使用Preconnect
<!-- 错误:对所有第三方域名使用preconnect,建立不必要的连接 -->
<link rel="preconnect" href="https://partner1.example.com">
<link rel="preconnect" href="https://partner2.example.com">
<link rel="preconnect" href="https://partner3.example.com">

问题:每个preconnect都会消耗客户端和服务端资源,建立不必要的连接会浪费内存和CPU资源,尤其是在移动设备上。

2.2.4 代码评审要点:预加载技术的「5个检查项」

评审维度检查要点工具支持
Preload使用1. Preload是否仅用于首屏必需的关键资源?
2. 是否设置了正确的as属性和type属性?
3. 字体preload是否添加了crossorigin属性?
1. Chrome DevTools → Network面板
2. Lighthouse的「Preload key requests」建议
Prefetch使用1. Prefetch的资源是否是用户高概率访问的?
2. 是否控制了prefetch的资源数量和体积?
3. 移动网络下是否限制了prefetch?
1. 分析用户行为数据(如热图、跳转路径)
2. 使用navigator.connection检测网络状况
跨域优化1. 主要第三方域名是否使用了preconnect?
2. 是否避免对低优先级域名使用preconnect?
3. dns-prefetch是否用于可能需要的域名?
1. 检查第三方资源的域名
2. Chrome DevTools → Timing面板(查看DNS/TCP耗时)
资源优先级1. 是否通过fetchpriority="high"提升关键资源优先级?
2. 是否避免对非关键资源设置高优先级?
1. Chrome DevTools → Network面板(查看Priority列)
2. WebPageTest的瀑布图
加载时机1. 预加载是否考虑了设备性能(如低端机减少prefetch)?
2. 是否根据用户行为动态触发预加载?
1. 使用navigator.deviceMemory检测设备性能
2. 分析用户交互日志

2.3 服务端优化:从「客户端渲染」到「服务端加速」

客户端渲染(CSR)需要浏览器下载HTML、CSS、JS后,在客户端完成页面组装,首屏加载时间较长。服务端优化技术(SSR、SSG、ISR)通过将部分渲染工作转移到服务端,显著减少客户端的渲染压力,从而加速首屏加载。

2.3.1 原理:不同渲染模式的「性能对比」

目前主流的渲染模式有四种,各有优劣:

渲染模式全称工作原理首屏速度SEO友好度开发复杂度适用场景
CSR客户端渲染服务端返回空HTML,客户端JS渲染页面慢(需下载并执行JS)差(爬虫可能不执行JS)后台管理系统、交互复杂的应用
SSR服务端渲染服务端生成完整HTML,客户端激活为SPA快(HTML直接可渲染)内容型网站(博客、电商)
SSG静态站点生成构建时预生成HTML,服务端直接返回最快(纯静态文件)最好营销页、文档站、博客
ISR增量静态再生预生成HTML,定时或按需重新生成快(接近SSG)内容频繁更新的静态站(新闻、电商)

服务端优化的核心优势在于:

  1. 减少首屏渲染时间:服务端直接返回可渲染的HTML,无需等待客户端JS执行;
  2. 改善SEO:搜索引擎爬虫可直接读取HTML内容;
  3. 降低客户端要求:低端设备无需执行大量JS即可显示页面。

2.3.2 代码样例:服务端优化技术实践

2.3.2.1 Vue3 + Nuxt3实现SSR(服务端渲染)

Nuxt3是Vue3的服务端渲染框架,简化了SSR的实现:

  1. 创建Nuxt3项目:
npx nuxi init nuxt-ssr-demo
cd nuxt-ssr-demo
npm install
  1. 创建首屏页面(pages/index.vue):
<!-- 符合谷歌Vue规范:模板结构清晰,逻辑与UI分离 -->
<template>
  <div class="home-page">
    <Header />
    <main class="main-content">
      <h1 class="page-title">欢迎来到首页</h1>
      <p class="page-description">{{ description }}</p>
      <ProductList :products="products" />
    </main>
    <Footer />
  </div>
</template>

<script setup lang="ts">
import Header from '~/components/Header.vue';
import Footer from '~/components/Footer.vue';
import ProductList from '~/components/ProductList.vue';
import { fetchProducts } from '~/api/products';

// 服务端获取数据(Nuxt3的useAsyncData)
const { data } = await useAsyncData('products', async () => {
  // 该请求在服务端执行,数据注入HTML
  const products = await fetchProducts();
  return { products };
});

// 页面描述(服务端渲染到HTML中)
const description = '这是一个使用Nuxt3 SSR的首页,首屏加载更快';

// 解构产品数据
const { products } = data.value;
</script>

<style scoped lang="scss">
.home-page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.main-content {
  flex: 1;
  padding: 24px;
}

.page-title {
  margin: 0 0 16px 0;
  font-size: 28px;
  font-weight: 600;
}

.page-description {
  margin: 0 0 24px 0;
  font-size: 16px;
  color: #666;
}
</style>
  1. 配置nuxt.config.ts启用SSR:
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true, // 启用SSR(默认值)
  nitro: {
    // 配置缓存策略(减少服务端渲染压力)
    cache: {
      driver: 'redis', // 使用Redis缓存渲染结果
      ttl: 60, // 缓存1分钟
    },
  },
  // 配置CDN,加速静态资源
  app: {
    cdnURL: 'https://cdn.example.com',
  },
});
  1. 启动服务:
npm run dev # 开发模式
npm run build # 构建生产版本
npm run preview # 预览生产版本
2.3.2.2 Next.js实现SSG(静态站点生成)

Next.js是React的服务端渲染框架,支持SSG:

  1. 创建Next.js项目:
npx create-next-app@latest next-ssg-demo --typescript
cd next-ssg-demo
  1. 创建静态页面(pages/index.tsx):
// pages/index.tsx(符合谷歌TS规范:类型明确,组件结构清晰)
import type { GetStaticProps } from 'next';
import Head from 'next/head';
import Header from '../components/Header';
import Footer from '../components/Footer';
import ProductCard from '../components/ProductCard';
import { Product } from '../types/product';
import { fetchProducts } from '../api/products';

// 页面Props类型
interface HomeProps {
  products: Product[];
  lastUpdated: string;
}

export default function Home({ products, lastUpdated }: HomeProps) {
  return (
    <div className="home-page">
      <Head>
        <title>首页 - 静态站点</title>
        <meta name="description" content="使用Next.js SSG生成的静态首页" />
      </Head>
      
      <Header />
      
      <main className="main-content">
        <h1 className="page-title">推荐产品</h1>
        <p className="update-info">最后更新:{lastUpdated}</p>
        
        <div className="product-grid">
          {products.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      </main>
      
      <Footer />
    </div>
  );
}

// 构建时获取数据(SSG核心)
export const getStaticProps: GetStaticProps = async () => {
  // 构建时执行,获取产品数据
  const products = await fetchProducts();
  const lastUpdated = new Date().toLocaleString();
  
  // 数据将被注入页面,构建为静态HTML
  return {
    props: {
      products,
      lastUpdated,
    },
    // 可选:设置重新生成时间(ISR特性)
    revalidate: 3600, // 每小时重新生成一次
  };
};
  1. 配置next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  // 配置图片优化(自动WebP转换、尺寸优化)
  images: {
    formats: ['image/avif', 'image/webp'],
    domains: ['cdn.example.com'], // 允许的图片域名
  },
  // 输出静态文件到out目录
  output: 'export',
};

module.exports = nextConfig;
  1. 构建静态文件:
npm run build # 生成静态HTML到out目录

构建完成后,out目录中会生成可直接部署的静态HTML文件,无需Node.js服务即可运行。

2.3.2.3 服务端渲染缓存策略(减轻服务端压力)

SSR虽然提升了首屏速度,但增加了服务端压力,合理的缓存策略至关重要:

// Nuxt3的Nitro配置(server/api/cached-product.ts)
export default cachedEventHandler(
  async (event) => {
    const id = getQuery(event).id as string;
    // 从数据库获取产品详情
    const product = await getProductById(id);
    return product;
  },
  {
    // 缓存键:基于产品ID
    key: (event) => `product:${getQuery(event).id}`,
    // 缓存时间:10分钟
    ttl: 60 * 10,
    // 仅缓存GET请求
    method: ['GET'],
    // 条件缓存:仅缓存状态码为200的响应
    statusCode: [200],
  }
);

在Next.js中使用ISR(增量静态再生):

// pages/products/[id].tsx
export async function getStaticProps({ params }) {
  const product = await fetchProductById(params.id);
  
  return {
    props: { product },
    // 每小时重新生成一次(ISR)
    revalidate: 3600,
  };
}

// 预生成热门产品页面
export async function getStaticPaths() {
  const popularProducts = await fetchPopularProducts();
  
  return {
    paths: popularProducts.map(product => ({
      params: { id: product.id.toString() }
    })),
    // 对未预生成的页面,在第一次请求时生成
    fallback: 'blocking'
  };
}
2.3.2.4 混合渲染:关键页面SSR,其他页面CSR

并非所有页面都需要服务端渲染,可根据页面重要性选择渲染模式:

// Nuxt3混合渲染配置(nuxt.config.ts)
export default defineNuxtConfig({
  routeRules: {
    // 首页和产品详情页使用SSR
    '/': { ssr: true },
    '/products/**': { ssr: true },
    
    // 购物车页面使用CSR(交互频繁,无需SEO)
    '/cart': { ssr: false },
    
    // 帮助页面使用SSG(静态内容)
    '/help/**': { static: true },
    
    // 新闻页面使用ISR(每10分钟更新)
    '/news/**': { isr: 600 }
  }
});

2.3.3 实践反例:服务端优化的「常见误区」

2.3.3.1 反例1:对所有页面使用SSR(增加服务端压力)
// 错误:所有页面都启用SSR,即使是不需要的后台页面
export default defineNuxtConfig({
  ssr: true, // 全局启用SSR
});

问题:SSR需要服务端执行JS生成HTML,对服务端CPU和内存消耗较大。后台管理系统等内部页面无需SEO,使用CSR即可,盲目使用SSR会浪费服务端资源。

2.3.3.2 反例2:服务端渲染未做缓存(性能瓶颈)
// 错误:SSR页面未做缓存,每次请求都重新渲染
export default async (req, res) => {
  // 每次请求都重新获取数据并渲染
  const data = await fetchAllData();
  const html = await renderToString(App, { data });
  res.send(html);
};

问题:高并发场景下,未缓存的SSR会导致服务端响应缓慢,甚至崩溃。研究表明,合理的缓存可减少90%以上的服务端渲染工作量。

2.3.3.3 反例3:服务端渲染包含大量客户端逻辑
<!-- 错误:SSR页面包含大量客户端初始化逻辑 -->
<template>
  <div class="complex-page">
    <!-- 页面内容 -->
  </div>
</template>

<script setup lang="ts">
// 服务端渲染后,客户端需要执行大量JS
onMounted(async () => {
  // 错误:在客户端初始化时加载大量数据
  await loadChartData();
  await initMap();
  await fetchUserBehavior();
  // ... 其他耗时操作
});
</script>

问题:SSR仅优化了首屏HTML的加载,但客户端激活(hydration)时执行大量JS会导致交互延迟(Time to Interactive, TTI指标变差)。

2.3.3.4 反例4:SSG页面包含动态内容(内容过时)
// 错误:使用SSG渲染包含实时数据的页面
export const getStaticProps = async () => {
  // 构建时获取一次数据,后续不会更新
  const realtimeData = await fetchRealtimeData(); // 如股票价格、实时排名
  
  return {
    props: { realtimeData },
  };
};

问题:SSG页面在构建时生成,后续不会更新。包含实时数据的页面使用SSG会导致内容过时,应使用SSR或客户端动态获取。

2.3.4 代码评审要点:服务端优化的「6个检查项」

评审维度检查要点工具支持
渲染模式选择1. 是否根据页面类型选择合适的渲染模式(SSR/SSG/CSR)?
2. 非关键页面是否避免使用SSR?
1. 分析页面的SEO需求和交互复杂度
2. 查看路由配置中的渲染模式
数据获取1. 服务端数据获取是否只获取首屏必需数据?
2. 是否避免在服务端获取客户端特有数据(如浏览器信息)?
1. 检查getServerSidePropsuseAsyncData中的数据请求
2. 分析服务端日志中的API调用
缓存策略1. SSR页面是否实现了合理的缓存?
2. ISR的重新生成时间是否合理?
3. 静态资源是否启用了长期缓存?
1. 检查服务端缓存配置(如Redis、CDN)
2. 使用Chrome DevTools查看响应头的Cache-Control
客户端激活1. 客户端激活(hydration)时间是否控制在500ms以内?
2. 是否避免在激活阶段执行耗时操作?
1. Lighthouse的「Time to Interactive」指标
2. Chrome DevTools → Performance面板
服务端性能1. 服务端渲染时间是否控制在100ms以内?
2. 是否避免在服务端执行复杂计算?
1. 服务端性能监控(如Node.js的process.hrtime
2. APM工具(如New Relic、Datadog)
降级策略1. 服务端渲染失败时是否有降级到CSR的策略?
2. 高负载时是否有合理的限流措施?
1. 检查服务端错误处理逻辑
2. 模拟服务端故障测试降级效果

2.4 缓存策略:让「重复访问」瞬间完成

缓存是提升重复访问性能的「利器」——通过存储已获取的资源,避免重复下载和计算,可使二次访问的首屏加载时间减少50%以上。有效的缓存策略需要结合HTTP缓存(静态资源)和本地缓存(API数据),在「新鲜度」和「性能」之间找到平衡。

2.4.1 原理:缓存的「存储层次」与「失效机制」

Web缓存可分为多个层次,从快到慢依次为:

  1. 内存缓存(Memory Cache):浏览器临时存储在内存中的资源,关闭标签页后失效,速度最快;
  2. 磁盘缓存(Disk Cache):浏览器存储在硬盘中的资源,关闭浏览器后仍存在,速度次之;
  3. HTTP缓存:通过HTTP头控制的缓存(Cache-Control、ETag等),由浏览器和中间代理(如CDN)实现;
  4. 本地缓存:开发者通过localStorage/sessionStorage/indexedDB手动存储的数据;
  5. 服务端缓存:如Redis缓存API响应,减少数据库查询。

缓存的核心挑战是「失效机制」——如何确保用户获取到最新内容的同时,最大化利用缓存。主要的缓存失效策略有:

  • 时间过期(TTL):设置缓存有效期(如max-age=3600),过期后重新请求;
  • 内容校验(Validation):使用ETag或Last-Modified,服务器验证内容是否变化;
  • 主动更新:内容变化时主动通知客户端更新缓存(如WebSocket);
  • 版本控制:资源文件名包含哈希(如app.a1b2c3.js),内容变化时文件名变化。

2.4.2 代码样例:缓存策略的最佳实践

2.4.2.1 HTTP缓存配置(Nginx示例)

通过Nginx配置HTTP缓存头,控制静态资源的缓存策略:

# nginx.conf(符合谷歌服务器配置规范:清晰的缓存分层)
server {
    listen 80;
    server_name example.com;
    
    # 根目录
    root /var/www/example.com;
    
    # 1. 静态资源缓存(图片、字体、视频)
    location ~* \.(jpg|jpeg|png|gif|webp|avif|ico|svg|woff2|woff|ttf|mp4|webm)$ {
        # 长期缓存(1年),因为文件名包含哈希
        expires 365d;
        add_header Cache-Control "public, max-age=31536000, immutable";
        # 启用gzip压缩
        gzip on;
        gzip_types image/jpeg image/png image/webp text/css application/javascript;
    }
    
    # 2. CSS和JS缓存
    location ~* \.(css|js)$ {
        # 长期缓存(1年),依赖文件名哈希实现失效
        expires 365d;
        add_header Cache-Control "public, max-age=31536000, immutable";
        # 启用gzip压缩
        gzip on;
        gzip_types text/css application/javascript;
    }
    
    # 3. HTML缓存(短期缓存,需要验证)
    location ~* \.(html|htm)$ {
        # 短期缓存(10分钟),过期后验证
        expires 10m;
        add_header Cache-Control "public, max-age=600, must-revalidate";
        # 启用ETag验证
        etag on;
        add_header ETag "$entity_tag";
    }
    
    # 4. API响应缓存(按需缓存)
    location /api/ {
        # 代理到后端服务
        proxy_pass http://backend:3000;
        
        # 对GET请求缓存5分钟
        if ($request_method = GET) {
            expires 5m;
            add_header Cache-Control "public, max-age=300, must-revalidate";
        }
        
        # 对不缓存的请求设置no-cache
        if ($request_method != GET) {
            add_header Cache-Control "no-store, no-cache, must-revalidate";
        }
    }
}
2.4.2.2 带哈希的资源文件名(Webpack配置)

通过文件名哈希实现缓存失效,确保用户获取最新内容:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    // 输出目录
    path: path.resolve(__dirname, 'dist'),
    // 带内容哈希的文件名:内容变化则哈希变化,缓存失效
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
    clean: true, // 构建前清理输出目录
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      // HTML不添加哈希,通过max-age=0确保每次请求
      inject: 'body', // JS注入到body底部
      minify: {
        removeComments: true,
        collapseWhitespace: true,
      },
    }),
  ],
  optimization: {
    // 提取runtime代码(避免哈希不必要的变化)
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};
2.4.2.3 API数据缓存(客户端实现)

使用localStorage结合TTL实现API数据缓存,减少重复请求:

// src/utils/apiCache.ts(符合谷歌TS规范:类型安全,错误处理完善)
interface CacheEntry<T> {
  data: T;
  timestamp: number;
  ttl: number; // 缓存有效期(秒)
}

/**
 * API数据缓存工具
 */
export const apiCache = {
  /**
   * 设置缓存
   * @param key 缓存键
   * @param data 缓存数据
   * @param ttl 有效期(秒),默认300秒(5分钟)
   */
  set<T>(key: string, data: T, ttl: number = 300): void {
    try {
      const entry: CacheEntry<T> = {
        data,
        timestamp: Date.now(),
        ttl,
      };
      localStorage.setItem(`api_cache_${key}`, JSON.stringify(entry));
    } catch (error) {
      console.error('Failed to set API cache:', error);
      // 处理存储满的情况
      if ((error as DOMException).name === 'QuotaExceededError') {
        this.clearExpired(); // 清除过期缓存
        try {
          this.set(key, data, ttl); // 再次尝试
        } catch (e) {
          console.error('Still failed to set API cache after clearing expired:', e);
        }
      }
    }
  },

  /**
   * 获取缓存
   * @param key 缓存键
   * @returns 缓存数据(未命中或过期则返回null)
   */
  get<T>(key: string): T | null {
    try {
      const entryStr = localStorage.getItem(`api_cache_${key}`);
      if (!entryStr) return null;

      const entry: CacheEntry<T> = JSON.parse(entryStr);
      const now = Date.now();
      
      // 检查是否过期
      if (now - entry.timestamp > entry.ttl * 1000) {
        this.remove(key); // 移除过期缓存
        return null;
      }
      
      return entry.data;
    } catch (error) {
      console.error('Failed to get API cache:', error);
      this.remove(key); // 缓存损坏,移除
      return null;
    }
  },

  /**
   * 移除缓存
   * @param key 缓存键
   */
  remove(key: string): void {
    localStorage.removeItem(`api_cache_${key}`);
  },

  /**
   * 清除所有缓存
   */
  clearAll(): void {
    const keys = Object.keys(localStorage);
    keys.forEach(key => {
      if (key.startsWith('api_cache_')) {
        localStorage.removeItem(key);
      }
    });
  },

  /**
   * 清除所有过期缓存
   */
  clearExpired(): void {
    const keys = Object.keys(localStorage);
    const now = Date.now();
    
    keys.forEach(key => {
      if (key.startsWith('api_cache_')) {
        try {
          const entryStr = localStorage.getItem(key);
          if (entryStr) {
            const entry = JSON.parse(entryStr) as CacheEntry<unknown>;
            if (now - entry.timestamp > entry.ttl * 1000) {
              localStorage.removeItem(key);
            }
          }
        } catch (error) {
          console.error(`Failed to check cache ${key}:`, error);
          localStorage.removeItem(key);
        }
      }
    });
  }
};

在API请求中使用缓存:

// src/api/products.ts
import { http } from './http';
import { apiCache } from '../utils/apiCache';
import { Product, ProductListResponse } from '../types/product';

/**
 * 获取产品列表(带缓存)
 */
export const getProducts = async (page = 1, limit = 10): Promise<ProductListResponse> => {
  const cacheKey = `products_page${page}_limit${limit}`;
  
  // 尝试从缓存获取
  const cachedData = apiCache.get<ProductListResponse>(cacheKey);
  if (cachedData) {
    console.log('Using cached products data');
    return cachedData;
  }
  
  // 缓存未命中,发起请求
  const response = await http.get<ProductListResponse>(`/api/products?page=${page}&limit=${limit}`);
  
  // 存入缓存(设置10分钟有效期)
  apiCache.set(cacheKey, response, 600);
  
  return response;
};

/**
 * 获取产品详情(带缓存)
 */
export const getProductDetail = async (id: string): Promise<Product> => {
  const cacheKey = `product_${id}`;
  
  // 尝试从缓存获取
  const cachedData = apiCache.get<Product>(cacheKey);
  if (cachedData) {
    console.log(`Using cached product ${id} data`);
    return cachedData;
  }
  
  // 缓存未命中,发起请求
  const response = await http.get<Product>(`/api/products/${id}`);
  
  // 存入缓存(设置30分钟有效期)
  apiCache.set(cacheKey, response, 1800);
  
  return response;
};
2.4.2.4 缓存更新策略(主动失效)

当数据更新时,主动失效相关缓存,确保数据一致性:

// src/api/productAdmin.ts
import { http } from './http';
import { apiCache } from '../utils/apiCache';
import { Product } from '../types/product';

/**
 * 更新产品信息
 */
export const updateProduct = async (id: string, data: Partial<Product>): Promise<Product> => {
  const response = await http.put<Product>(`/api/products/${id}`, data);
  
  // 主动失效相关缓存
  apiCache.remove(`product_${id}`); // 失效单个产品缓存
  
  // 失效所有产品列表缓存(简单粗暴,也可更精确)
  for (let page = 1; page <= 10; page++) { // 假设最多10页
    apiCache.remove(`products_page${page}_limit10`);
    apiCache.remove(`products_page${page}_limit20`);
  }
  
  return response;
};

/**
 * 创建新产品
 */
export const createProduct = async (data: Omit<Product, 'id'>): Promise<Product> => {
  const response = await http.post<Product>('/api/products', data);
  
  // 失效所有产品列表缓存
  for (let page = 1; page <= 10; page++) {
    apiCache.remove(`products_page${page}_limit10`);
    apiCache.remove(`products_page${page}_limit20`);
  }
  
  return response;
};

2.4.3 实践反例:缓存策略的「常见错误」

2.4.3.1 反例1:静态资源未加哈希且缓存时间过长
# 错误:静态资源未加哈希但设置长期缓存
location ~* \.(css|js)$ {
    expires 365d; # 长期缓存
    add_header Cache-Control "public, max-age=31536000";
}

问题:当CSS/JS内容更新时,由于文件名未变,浏览器会使用旧缓存,导致用户无法获取最新内容,必须强制刷新才能生效。

2.4.3.2 反例2:对API响应设置过长缓存且无失效机制
// 错误:API缓存时间过长且无失效机制
export const getUserInfo = async (userId: string) => {
  const cacheKey = `user_${userId}`;
  const cachedData = apiCache.get(cacheKey);
  
  if (cachedData) {
    return cachedData;
  }
  
  const data = await http.get(`/api/users/${userId}`);
  apiCache.set(cacheKey, data, 86400); // 缓存24小时,无主动失效
  
  return data;
};

问题:当用户信息更新(如头像、昵称变化),客户端会在24小时内显示旧数据,导致数据不一致。

2.4.3.3 反例3:对所有资源使用no-cache(完全禁用缓存)
# 错误:全局禁用缓存
location / {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    expires 0;
}

问题:完全禁用缓存会导致每次访问都重新下载所有资源,显著增加加载时间和带宽消耗,尤其是重复访问时体验极差。

2.4.3.4 反例4:滥用localStorage存储大量数据
// 错误:用localStorage存储大量数据(如1000条产品记录)
export const cacheAllProducts = async () => {
  const allProducts = await http.get('/api/products?limit=1000');
  // localStorage单个域名通常限制5MB,大量数据会导致存储失败
  localStorage.setItem('all_products', JSON.stringify(allProducts));
};

问题:localStorage有大小限制(通常5MB),存储大量数据会导致QuotaExceededError,且读取大型JSON的性能较差(阻塞主线程)。

2.4.4 代码评审要点:缓存策略的「6个检查项」

评审维度检查要点工具支持
静态资源缓存1. 静态资源(CSS/JS/图片)是否添加内容哈希?
2. 是否对不同类型资源设置合理的缓存时间?
3. HTML是否设置较短的缓存时间或验证机制?
1. 查看构建输出的文件名(如app.a1b2c3.js
2. Chrome DevTools → Network → Response Headers
API缓存1. 是否对GET请求设置合理的缓存?
2. POST/PUT/DELETE请求是否禁用缓存?
3. 缓存有效期是否根据数据更新频率设置?
1. 检查API响应头的Cache-Control
2. 查看客户端缓存工具的使用(如apiCache)
缓存失效1. 数据更新时是否主动失效相关缓存?
2. 静态资源是否通过文件名哈希实现失效?
3. 是否有缓存清理机制(如清除过期缓存)?
1. 检查数据更新接口的缓存处理逻辑
2. 测试内容更新后是否能获取最新版本
本地缓存1. localStorage是否只存储小体积数据?
2. 是否处理localStorage的存储限制(QuotaExceededError)?
3. 敏感数据是否避免存储在localStorage?
1. 检查localStorage的使用场景和数据大小
2. 模拟存储满的情况测试错误处理
缓存一致性1. 客户端缓存与服务端数据是否保持一致?
2. 是否有机制处理缓存数据过时的情况?
1. 测试数据更新后客户端的表现
2. 检查缓存验证机制(如ETag)
缓存监控1. 是否监控缓存命中率?
2. 是否有缓存相关的错误监控?
1. 实现缓存命中率统计
2. 查看错误日志中的缓存相关错误

对话小剧场:首屏加载优化的「团队攻坚」

场景:电商项目经过包体积优化后,首屏加载时间从6秒降到了3秒,但团队认为仍有优化空间,目标是1.5秒以内。前端团队(小美、小迪、小稳)、后端(大熊)、QA(小燕)再次召开优化会议。

参会人员

  • 小美(前端开发):负责业务开发;
  • 小迪(前端开发):负责性能优化;
  • 小稳(前端架构师):负责技术选型;
  • 大熊(后端开发):负责服务端和CDN;
  • 小燕(QA):负责性能测试。

小燕(QA):现状分析,明确目标

“根据最新的测试数据,包体积优化后首屏加载时间稳定在2.8-3.2秒之间,主要瓶颈在:

  1. 首屏CSS和JS的加载耗时约1.2秒;
  2. LCP图片(首页Banner)加载耗时约800ms;
  3. 首次访问时,第三方CDN的连接建立耗时约300ms;
  4. 重复访问时,缓存命中率只有40%,很多资源需要重新加载。
    我们的目标是将首屏加载时间控制在1.5秒以内,重复访问控制在500ms以内。”

小迪(前端开发):前端优化方案

“我分析了网络瀑布图,发现关键资源的加载顺序可以优化:

  1. 目前首屏CSS是外部文件,需要额外请求,我建议把首屏必需的CSS内联到HTML中,预计能节省300ms;
  2. LCP图片现在是普通加载,改成preload优先加载,同时换成WebP格式,体积能减少40%,预计节省400ms;
  3. 非关键JS(如统计、聊天工具)没有用async/defer,阻塞了HTML解析,改成async加载,预计节省200ms;
  4. 对用户可能点击的「热销商品」区域图片进行prefetch,提升后续交互速度。”

大熊(后端开发):服务端与CDN优化

“服务端这边可以配合:

  1. 启用SSR渲染首页和产品列表页,首屏HTML直接返回渲染好的内容,不需要等客户端JS执行,预计能减少500ms;
  2. CDN配置优化:静态资源开启Brotli压缩,比gzip再小15-20%;对我们的主要CDN域名(cdn.example.com)设置preconnect,减少连接建立时间;
  3. 缓存策略调整:静态资源(带哈希)设置1年缓存,HTML设置10分钟缓存+ETag验证,API响应根据更新频率设置5-30分钟缓存;
  4. 实施HTTP/2多路复用,减少连接开销,尤其是并行加载多个小资源时效果明显。”

小稳(前端架构师):架构层面优化

“从架构角度,我补充两点:

  1. 实施「核心渲染路径」优化:减少首屏关键资源数量,目前有8个关键资源,目标减少到4个以内;控制关键资源体积总和在150KB以下(gzip后);
  2. 首页采用混合渲染策略:首屏内容用SSR,保证快速呈现;用户滚动后才加载的内容(如页脚、推荐商品)用CSR,减少服务端压力;
  3. 实现智能预加载:根据用户行为数据,对高概率访问的页面(如「新品上架」)进行prefetch,当用户浏览首页超过3秒且停留在内页入口附近时触发;
  4. 对低端设备和弱网环境,自动降级到轻量版首页(减少动画、简化布局),保证基础体验。”

小美(前端开发):落地执行计划

“我来整理具体的执行步骤:

  1. 本周一、二:我负责将首屏CSS内联,非关键JS添加async/defer,LCP图片preload和格式转换;
  2. 本周三、四:大熊负责SSR部署和CDN配置优化,小迪负责智能预加载逻辑实现;
  3. 本周五:小稳负责混合渲染策略落地,我配合调整组件;
  4. 下周一:小燕进行全面测试,包括首屏时间、缓存命中率、各浏览器兼容性;
  5. 下周二:根据测试结果微调,准备上线。”

小燕(QA):测试方案与验收标准

“我会制定详细的测试方案:

  1. 测试环境:4G网络(30Mbps下载,10Mbps上传,50ms延迟)、3G网络(3Mbps下载,1Mbps上传,300ms延迟);
  2. 测试设备:高端机(iPhone 14/小米13)、中端机(iPhone 11/红米Note 10)、低端机(iPhone SE/红米9);
  3. 验收标准:
    • 首屏加载时间:4G环境≤1.5秒,3G环境≤3秒;
    • 缓存命中率:重复访问≥80%;
    • LCP时间:≤1.2秒;
    • TTI(可交互时间):≤2秒。”

两周后:优化方案上线,首屏加载时间从3秒降到1.3秒,重复访问时间降到400ms,LCP时间800ms,各项指标均达到预期。用户留存率提升了15%,转化率提升了8%。

总结:首屏加载加速的「黄金法则」

首屏加载加速是一项系统工程,需要前端、后端、运维协同配合,其核心可归纳为「黄金法则」:

  1. 优先传输关键资源:识别首屏必需的HTML、CSS、JS和图片,减少数量、压缩体积、优化加载顺序;
  2. 提前预判用户需求:合理使用preload/prefetch/preconnect,在用户需要前加载资源;
  3. 分担客户端压力:对核心页面采用SSR/SSG,减少客户端渲染工作量;
  4. 最大化利用缓存:结合HTTP缓存和本地缓存,让重复访问「瞬间完成」;
  5. 按需降级:根据设备性能和网络状况,提供适配的加载策略。

首屏加载优化没有终点,需要持续监控、分析和迭代。下一篇,我们将探讨「感知性能优化」——如何让用户「感觉」页面更快,即使实际加载时间没有变化。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐