本文将详细讲解前端性能优化的基础概念,包括代码压缩、图片压缩、缓存策略等内容,适合初学者阅读。

1.简述前端性能优化的重要性,性能优化的核心指标有哪些(如 FCP、LCP、FID、CLS)?

一、前端性能优化的重要性

前端性能直接决定用户对产品的第一印象和使用体验,同时深刻影响业务转化、SEO 排名及技术成本,其重要性可从以下 4 个核心维度展开:

1. 提升用户体验,减少用户流失

  • 页面加载慢(如超过 3 秒)会导致 70% 以上的用户放弃访问(谷歌数据);
  • 交互响应延迟(如点击按钮后 1 秒才反馈)会让用户感知“卡顿”,降低产品易用性;
  • 布局偏移(如图片加载后挤压文字)会导致用户误操作(如点错按钮),破坏使用流畅性。

2. 提升业务转化,降低获客成本

  • 电商场景:页面加载速度每提升 1 秒,转化率可提升 7%(亚马逊数据);
  • 内容场景:加载快的页面用户停留时间更长,广告曝光和内容消费率更高;
  • 性能差的产品会导致用户复购率下降,间接增加获客成本(需不断拉新弥补流失)。

3. 优化 SEO 排名,获取更多自然流量

  • 谷歌自 2021 年起将 Core Web Vitals(核心 Web 指标) 纳入搜索排名因素,性能差的页面会被降权;
  • 加载快的页面更易被搜索引擎爬虫抓取(爬虫抓取效率更高),内容收录更全面。

4. 降低技术成本,减少资源消耗

  • 性能优化(如压缩资源、按需加载)可减少服务器带宽消耗和 CDN 流量成本;
  • 移动端场景下,优化后的页面能减少用户流量消耗,提升低网速/低配置设备的兼容性。

二、前端性能优化的核心指标

前端性能指标分为 “加载性能”“交互性能”“视觉稳定性” 三大类,其中 Core Web Vitals(核心 Web 指标) 是谷歌推荐的核心衡量标准,此外还有辅助性关键指标:

1. 核心 Web 指标(Core Web Vitals)

谷歌定义的“用户体验核心指标”,直接反映用户对页面性能的真实感知:

指标名称 英文缩写 指标定义 核心意义 理想目标值
最大内容绘制 LCP 页面开始加载到“最大内容元素”(如主图、大标题)完全渲染的时间 衡量 加载性能 ≤ 2.5 秒(优秀)
首次输入延迟 FID 用户首次与页面交互(如点击按钮、输入文字)到浏览器响应的时间 衡量 交互响应性 ≤ 100 毫秒(优秀)
累积布局偏移 CLS 页面加载过程中“布局意外偏移”的总和(计算元素位置变化的影响范围) 衡量 视觉稳定性 ≤ 0.1(优秀)

补充:FID 已逐步被 INP(Interaction to Next Paint,下一次绘制的交互延迟) 替代,INP 衡量“所有用户交互”的响应延迟,更全面反映交互流畅性,理想值 ≤ 200 毫秒。

2. 辅助关键指标(补充衡量维度)

  • 首次内容绘制(FCP):页面开始加载到“第一个内容元素”(如文字、小图标)渲染的时间,反映“页面是否开始加载”,理想值 ≤ 1.8 秒;
  • 首次字节时间(TTFB):浏览器发送请求到接收服务器“第一个字节数据”的时间,反映 服务器响应速度,理想值 ≤ 600 毫秒;
  • 可交互时间(TTI):页面加载到“所有脚本加载完成、可稳定交互”的时间,反映“页面是否真正可用”,理想值 ≤ 3.8 秒;
  • 首次有意义绘制(FMP):页面加载到“用户感知到‘核心内容’已呈现”的时间(如列表首屏、表单主体),比 FCP 更贴近“内容可用性”。

总结

前端性能优化的核心是“以用户为中心”——通过优化 LCP、FID/INP、CLS 等核心指标,解决“加载慢、交互卡、布局跳”三大痛点,最终实现“用户体验提升→业务转化增长→技术成本降低”的正向循环。

2.什么是 FCP(首次内容绘制)、LCP(最大内容绘制)?如何优化这两个指标?

一、FCP(首次内容绘制)与 LCP(最大内容绘制)的定义

两者均属于 前端加载性能指标,但聚焦的“内容阶段”和用户感知价值不同,核心区别在于“内容的量级和核心性”:

1. FCP(First Contentful Paint,首次内容绘制)

  • 定义:从浏览器开始加载页面,到 第一个“有意义的内容元素”(如文字、图片、SVG、非白色背景的canvas)在屏幕上完成绘制 的时间。
  • 用户感知:FCP 是用户对“页面是否开始加载”的第一个直观反馈——只要看到任何非空白内容(哪怕是一个小图标或一行文字),就说明页面不再是“白屏”,加载已启动。
  • 注意:FCP 不关注内容大小,只关注“首次出现”;若页面仅加载了空白背景或纯颜色,不算 FCP(需有“内容元素”)。

2. LCP(Largest Contentful Paint,最大内容绘制)

  • 定义:从页面开始加载,到 视口中“最大的内容元素”(如首屏主图、大标题、长列表首屏区域)完全渲染 的时间。
  • 用户感知:LCP 反映“核心内容是否可用”——用户访问页面的核心目的是获取关键信息(如文章正文、商品主图、表单主体),LCP 完成意味着“核心内容已呈现,页面基本可用”。
  • 常见 LCP 元素
    • 图片(<img> 标签、背景图);
    • 文字块(<p><h1> 等包含大量文字的元素);
    • 视频(视频封面图)。

二、FCP 与 LCP 的优化策略

两者优化有 共性逻辑(均需减少“资源加载延迟”和“渲染阻塞”),但 LCP 需额外关注“核心大内容的加载优先级”,具体策略如下:

1. 通用优化:减少资源加载延迟与渲染阻塞(同时提升 FCP 和 LCP)

(1)压缩并精简首屏资源体积
  • 代码压缩
    • JS/CSS:用 Terser(压缩 JS)、CSSNano(压缩 CSS)移除冗余代码、注释、空格;
    • HTML:用 html-minifier 压缩 HTML 结构。
  • 图片优化
    • 格式:首屏图片优先用 WebP/AVIF(比 JPG/PNG 小 25%-50%),降级兼容旧浏览器;
    • 尺寸:按“视口实际需求”提供图片(如首屏主图无需 4K 分辨率,用 srcset 适配不同设备);
    • 压缩工具:用 Squoosh(在线)、Sharp(后端)压缩图片质量(保留视觉可接受的清晰度)。
(2)优化资源加载优先级:优先加载首屏关键资源
  • 预加载关键资源:用 <link rel="preload"> 强制浏览器优先加载首屏必需资源(如 LCP 图片、关键 CSS/JS),避免“关键资源被非关键资源阻塞”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
      <!-- 预加载 LCP 图片 -->
    <link rel="preload" as="image" href="/images/main-hero.webp" imagesrcset="/images/main-hero-480.webp 480w, /images/main-hero-800.webp 800w" imagesizes="100vw">
    <!-- 预加载关键 CSS -->
    <link rel="preload" as="style" href="/css/critical.css" onload="this.onload=null;this.rel='stylesheet'">
    ```

    - **延迟加载非关键资源**:
    - 非首屏图片/视频:加 `loading="lazy"` 实现懒加载(滚动到视口再加载);
    - 非关键 JS/CSS:用 `defer`(JS 延迟执行,不阻塞 DOM 解析)、`async`(JS 异步执行,加载完立即执行)或动态导入(`import()`),避免阻塞首屏渲染:

    ```html
    <!-- 非关键 JS 延迟加载 -->
    <script src="/js/analytics.js" defer></script>
    ```

    ##### (3)减少渲染阻塞:避免 CSS/JS 阻塞首屏绘制

    - **内联关键 CSS**:将首屏渲染必需的 CSS(如 body 样式、导航样式)直接内联到 `<head>` 的 `<style>` 标签中,避免“浏览器等待外部 CSS 加载完成才开始绘制”;非关键 CSS(如页脚、详情页样式)异步加载:

    ```html
    <head>
    <!-- 内联关键 CSS -->
    <style>
    body { margin: 0; padding: 0; background: #fff; }
    .header { height: 60px; line-height: 60px; }
    </style>
    <!-- 异步加载非关键 CSS -->
    <link rel="preload" as="style" href="/css/non-critical.css" onload="this.onload=null;this.rel='stylesheet'">
    </head>
    ```

    - **避免 JS 阻塞 DOM 解析**:
    - 把非关键 JS 放在 `<body>` 底部;
    - 关键 JS(如初始化首屏交互的代码)用 `defer` 或 `async`,或内联到 `<body>` 末尾(确保 DOM 已解析完成)。

    ##### (4)优化服务器响应速度(减少 TTFB)

    - **CDN 加速**:将静态资源(JS/CSS/图片)部署到 CDN,让用户从“最近的节点”加载资源,降低网络延迟;
    - **服务器优化**:
    - 用 HTTP/2 或 HTTP/3(支持多路复用,减少连接建立时间);
    - 启用 Gzip 或 Brotli 压缩(压缩传输的资源体积,减少下载时间);
    - 优化后端接口:减少数据库查询耗时、缓存热点数据(如用 Redis 缓存首屏数据),降低服务器响应时间(TTFB 理想值 ≤ 600ms)。

    #### 2. 针对 LCP 的额外优化:确保核心大内容优先加载

    LCP 优化的核心是“让最大内容元素尽快渲染”,需针对性解决“大内容加载慢”的问题:

    ##### (1)优先加载 LCP 元素,避免动态生成 LCP 元素

    - 若 LCP 是图片:避免用 JS 动态插入 `<img>` 标签(如 `document.createElement('img')`),直接在 HTML 中写死 `<img>` 标签,让浏览器更早发现并加载;
    - 若 LCP 是背景图:避免用 CSS `background-image` 加载(CSS 加载可能滞后),优先用 `<img>` 标签(可被 `preload` 优先加载),或用 `link rel="preload"` 预加载背景图。

    ##### (2)避免 LCP 元素被“渲染阻塞”

    - 若 LCP 是文字:确保字体文件优先加载,避免“文字闪烁”(FOIT/FOUT):
    - 用 `font-display: swap`(字体加载完成前显示系统默认字体,加载后替换);
    - 预加载关键字体:

    ```html
    <link rel="preload" as="font" href="/fonts/main-font.woff2" type="font/woff2" crossorigin>
    ```

    - 若 LCP 是图片:避免给图片加“延迟加载”(`loading="lazy"`)——首屏 LCP 图片必须“立即加载”,懒加载会导致 LCP 延迟。

    ##### (3)用 SSR/SSG 减少“数据加载→内容渲染”的延迟

    - 若页面核心内容(如 LCP 对应的文字/图片 URL)依赖后端数据:
    - 用 **SSR(服务端渲染)**:服务器提前渲染包含核心内容的 HTML,浏览器加载后直接渲染,无需等待 JS 拉取数据后再生成内容;
    - 用 **SSG(静态站点生成)**:构建时预渲染页面(如 VuePress、Next.js 静态生成),页面加载时无需后端动态生成,直接加载静态 HTML,LCP 可大幅提前。

    ### 三、指标检测与验证

    优化后需通过工具验证效果,确保指标达标(FCP 理想值 ≤ 1.8s,LCP 理想值 ≤ 2.5s):

    - **Chrome DevTools**:打开「Performance」面板,勾选「Screenshots」,刷新页面,可查看 FCP 和 LCP 的具体时间点及瓶颈(如“资源加载耗时过长”“渲染阻塞”);
    - **Lighthouse**:Chrome 扩展或 CLI 工具,运行性能检测后,会给出 FCP/LCP 得分及优化建议;
    - **Web Vitals API**:在代码中集成 API,实时监控用户真实环境下的 FCP/LCP 数据(用于生产环境优化验证)。

    ### 总结

    - FCP 优化核心:**尽早让“第一个内容”呈现**,重点解决“白屏时间长”,需减少渲染阻塞和首屏资源体积;
    - LCP 优化核心:**让“核心大内容”尽快加载完成**,重点提升 LCP 元素的加载优先级,避免动态生成或阻塞 LCP 元素;
    - 两者优化需结合“资源体积压缩、加载优先级调整、服务器加速”,最终目标是让用户“快速看到内容,快速用核心功能”。

    ## 3.什么是 FID(首次输入延迟)、CLS(累积布局偏移)?如何优化这两个指标?

    ### 一、FID 与 CLS 的定义

    两者均属于用户体验核心指标,分别聚焦“交互响应性”和“视觉稳定性”,直接影响用户对页面“流畅度”和“可靠性”的感知:

    #### 1. FID(First Input Delay,首次输入延迟)

    - **定义**:测量用户**首次与页面交互**(如点击按钮、输入文字、触摸屏幕)到浏览器**开始处理该交互**的时间差。
    - **核心意义**:反映页面对用户操作的“即时响应能力”。FID 高意味着用户操作后,页面长时间无反馈(如点击按钮后 500ms 才开始响应),会让用户感觉“页面卡顿、不灵敏”。
    - **本质原因**:FID 延迟主要由**主线程繁忙**导致——若浏览器主线程正忙于执行大量 JS(如解析大文件、复杂计算),则无法及时处理用户输入事件,从而产生延迟。

    #### 2. CLS(Cumulative Layout Shift,累积布局偏移)

    - **定义**:测量页面**整个生命周期内所有意外布局偏移的总和**,计算方式为“元素偏移的面积 × 偏移距离比例”(范围 0~1,值越小越稳定)。
    - **核心意义**:反映页面布局的“稳定性”。CLS 高意味着页面加载过程中元素频繁“跳动”(如图片加载后挤压文字、按钮突然移位),可能导致用户误操作(如点错按钮)或阅读中断,破坏使用体验。
    - **常见场景**:图片/视频未设置尺寸导致加载后撑开布局、动态插入内容(如广告)推挤现有元素、字体加载导致文字闪烁移位等。

    ### 二、FID 与 CLS 的优化策略

    #### 1. FID 优化:减少主线程阻塞,提升交互响应速度

    FID 的核心问题是“主线程被 JS 执行占用,无法及时处理用户输入”,优化需围绕“减轻主线程负担”展开:

    ##### (1)减少 JS 执行时间,避免长任务(Long Tasks)

    - **代码分割与懒加载**:
    - 用路由懒加载(如 Vue 的 `defineAsyncComponent`、React 的 `React.lazy`)将非首屏 JS 拆分,仅加载当前页面必需代码;
    - 对大型库(如图表库、地图 SDK)按需导入,避免一次性加载全量代码。
    - **优化 JS 执行效率**:
    - 避免不必要的全局变量和闭包(减少内存占用和垃圾回收压力);
    - 简化复杂计算(如用 `requestAnimationFrame` 拆分长循环,避免单次计算超过 50ms):

    ```javascript
    // 反例:长任务阻塞主线程
    for (let i = 0; i < 1000000; i++) { /* 复杂计算 */ }

    // 正例:拆分任务,避免阻塞
    let i = 0;
    function process() {
    const batch = Math.min(1000, 1000000 - i);
    for (let j = 0; j < batch; j++) { /* 处理部分任务 */ }
    i += batch;
    if (i < 1000000) {
    requestIdleCallback(process); // 利用浏览器空闲时间处理
    }
    }
    process();
    ```

    ##### (2)使用 Web Workers 分担计算压力

    将 CPU 密集型任务(如数据处理、复杂动画计算)移至 Web Workers(独立线程),避免阻塞主线程:

    ```javascript
    // 主线程:创建 Worker 并发送任务
    const worker = new Worker('data-processor.js');
    worker.postMessage(largeDataset); // 发送数据
    worker.onmessage = (e) => {
    console.log('处理结果:', e.data); // 接收结果,不阻塞主线程
    };

    // data-processor.js(Worker 线程):处理计算
    self.onmessage = (e) => {
    const result = heavyCalculation(e.data); // 复杂计算在独立线程执行
    self.postMessage(result);
    };
    ```

    ##### (3)优化事件监听器,避免高频触发

    - 对 `resize`、`scroll` 等高频触发事件,用**防抖(debounce)** 或**节流(throttle)** 限制执行频率:

    ```javascript
    // 节流:100ms 内只执行一次
    let lastTime = 0;
    window.addEventListener('scroll', (e) => {
    const now = Date.now();
    if (now - lastTime > 100) {
    handleScroll(e); // 处理滚动逻辑
    lastTime = now;
    }
    });
    ```

    - 避免在 `touchstart`、`mousedown` 等“即时响应”事件中执行复杂逻辑(优先用 `passive: true` 提升滚动流畅性)。

    #### 2. CLS 优化:减少布局偏移,保持视觉稳定性

    CLS 的核心是“避免元素尺寸或位置的意外变化”,需通过提前规划布局、稳定元素位置实现:

    ##### (1)为媒体元素(图片/视频/iframe)设置固定尺寸或占位符

    - 明确指定 `width` 和 `height`,或用 CSS `aspect-ratio` 定义宽高比,确保加载前后尺寸一致:

    ```html
    <!-- 方法 1:直接设置宽高 -->
    <img src="hero.jpg" width="800" height="400" alt="主图">

    <!-- 方法 2:用 aspect-ratio 定义比例(现代浏览器支持) -->
    <style>
    .hero-img {
    width: 100%;
    aspect-ratio: 2/1; /* 宽高比 2:1 */
    object-fit: cover; /* 图片自适应 */
    }
    </style>
    <img src="hero.jpg" class="hero-img" alt="主图">
    ```

    - 对动态加载的图片,用**骨架屏**或**占位容器**预留空间,避免加载后突然撑开布局。

    ##### (2)避免动态插入内容到现有内容上方

    - 广告、弹窗等动态内容应插入到页面底部或独立区域,若必须插入到现有内容中,需提前预留空间(如固定高度的容器);
    - 通知类组件(如“操作成功”提示)优先用“固定定位”(`position: fixed`)显示在页面角落,避免推挤其他元素。

    ##### (3)优化字体加载,避免文字闪烁移位(FOIT/FOUT)

    - 用 `font-display: swap` 配置字体加载策略:字体加载完成前显示系统默认字体(避免空白),加载后无缝替换(减少布局偏移):

    ```css
    @font-face {
    font-family: 'MyFont';
    src: url('myfont.woff2') format('woff2');
    font-display: swap; /* 关键:避免字体加载期间的布局偏移 */
    }
    ```

    - 预加载关键字体(如标题字体),减少加载延迟:

    ```html
    <link rel="preload" as="font" href="myfont.woff2" type="font/woff2" crossorigin>
    ```

    ##### (4)使用稳定的动画和过渡效果

    - 避免用 `top`、`left`、`width`、`height` 等属性修改元素位置或尺寸(会触发重排,导致布局偏移);
    - 优先用 `transform`(`translate`、`scale`)和 `opacity` 实现动画(仅触发重绘,不影响布局):

    ```css
    /* 反例:修改 top 导致布局偏移 */
    .box { transition: top 0.3s; }
    .box:hover { top: 10px; }

    /* 正例:用 transform 避免布局变化 */
    .box { transition: transform 0.3s; }
    .box:hover { transform: translateY(10px); }
    ```

    ### 三、指标检测与验证

    优化后可通过以下工具验证效果(FID 理想值 ≤ 100ms,CLS 理想值 ≤ 0.1):

    - **Chrome DevTools**:
    - FID:「Performance」面板录制交互过程,查看“Main”线程中“Input Delay”的耗时;
    - CLS:「Lighthouse」面板运行性能检测,查看“Cumulative Layout Shift”得分及具体偏移元素。
    - **Web Vitals 扩展**:Chrome 扩展“Web Vitals”可实时显示当前页面的 FID 和 CLS 数值。

    ### 总结

    - FID 优化核心:**减轻主线程负担**(减少 JS 执行时间、用 Web Workers 分担计算、优化事件监听),确保用户操作能被及时响应;
    - CLS 优化核心:**保持布局稳定**(为媒体元素设固定尺寸、避免动态内容推挤、优化字体加载),让用户操作和阅读不被意外偏移干扰。

    两者共同决定用户对页面“流畅度”和“可靠性”的感知,是前端体验优化的核心方向。

    ## 4.如何通过网络层面优化前端性能?(如减少 HTTP 请求、使用 HTTP/2、CDN 加速)

    网络层面是前端性能优化的核心环节,直接影响资源加载速度和用户等待时间。优化的核心思路是**减少请求开销、提升传输效率、缩短资源获取距离**,具体策略如下:

    ### 一、减少 HTTP 请求数量(降低连接成本)

    每次 HTTP 请求都需经历 DNS 解析、TCP 握手、数据传输等过程(尤其 HTTPS 还需 TLS 握手),请求越多,总开销越大。减少请求数量可显著降低网络延迟:

    #### 1. 资源合并与压缩

    - **JS/CSS 合并**:将多个小 JS 文件(如 `a.js`、`b.js`)合并为一个大文件(如 `bundle.js`),多个 CSS 文件合并为 `bundle.css`,减少请求次数(需注意:合并后体积不宜过大,避免单次加载耗时过长,可按路由或页面拆分)。
    - **图片合并(CSS Sprites)**:将多个小图标(如按钮图标、导航图标)合并为一张“雪碧图”,通过 CSS `background-position` 定位显示,将多次图片请求减少为 1 次(适合图标数量少的场景,现代项目更多用 iconfont 或 SVG 替代)。
    - **使用 Data URI**:将极小图片(如 1x1 像素点、小图标)转换为 Data URI(`data:image/png;base64,...`)嵌入 HTML/CSS 中,避免额外请求(注意:仅适合 < 10KB 的图片,否则会增加 HTML/CSS 体积)。

    #### 2. 按需加载资源(延迟非必要请求)

    - **路由懒加载**:仅加载当前页面所需的 JS/CSS,非首屏路由的资源在用户跳转时再加载(如 Vue 的 `defineAsyncComponent`、React 的 `React.lazy`)。
    - **图片/视频懒加载**:为非首屏图片添加 `loading="lazy"` 属性,或通过 IntersectionObserver API 监听元素进入视口后再加载,减少初始请求:

    ```html
    <!-- 原生懒加载(现代浏览器支持) -->
    <img src="non-critical.jpg" loading="lazy" alt="非首屏图片">
    ```

    ### 二、使用 HTTP/2(提升并行请求效率)

    HTTP/1.1 存在**请求阻塞**(同一域名下并行请求数限制,通常 6-8 个)和**头部冗余**(每次请求重复发送大量相同头部)等问题,HTTP/2 通过以下特性解决这些痛点:

    #### 1. HTTP/2 的核心优势

    - **多路复用**:同一 TCP 连接中可并行传输多个请求/响应,无需建立多个连接,避免“队头阻塞”(一个请求慢导致后续请求排队)。
    - **二进制分帧**:将数据分割为二进制帧传输,比 HTTP/1.1 的文本格式更高效,减少解析开销。
    - **头部压缩(HPACK)**:对请求头进行压缩(如缓存重复的 `User-Agent`、`Cookie` 等),减少头部传输体积。
    - **服务器推送**:服务器可主动向客户端推送关联资源(如请求 `index.html` 时,主动推送 `style.css` 和 `app.js`),无需等待客户端请求。

    #### 2. 如何启用 HTTP/2

    - 需服务器支持(如 Nginx、Apache、CDN 等),并配置 TLS 证书(大部分浏览器仅在 HTTPS 环境下支持 HTTP/2)。
    - Nginx 配置示例(启用 HTTP/2):

    ```nginx
    server {
    listen 443 ssl http2; # 关键:添加 http2 标识
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    # 其他配置...
    }
    ```

    ### 三、CDN 加速(缩短资源获取距离)

    CDN(内容分发网络)通过**全球分布式边缘节点**缓存静态资源,用户请求时从“最近的节点”获取资源,而非源服务器,大幅减少跨地域网络延迟。

    #### 1. CDN 的优化原理

    - **边缘节点缓存**:静态资源(JS、CSS、图片、视频)被缓存到离用户最近的节点(如用户在上海,从上海节点获取,而非北京源服务器),减少物理传输距离。
    - **负载均衡**:CDN 自动将请求分配到空闲节点,避免源服务器压力过大导致的响应延迟。

    #### 2. CDN 最佳实践

    - **缓存静态资源**:仅将**不常变化的静态资源**(如库文件、图片、字体)放入 CDN,动态内容(如接口数据)不适合缓存。
    - **合理设置缓存策略**:通过 `Cache-Control` 或 `Expires` 头设置缓存过期时间,减少重复回源请求(回源指节点无缓存时向源服务器请求):

    ```http
    # 对 JS/CSS 设置长期缓存(1 年),配合文件指纹(如 app.8f3d.js)确保更新生效
    Cache-Control: public, max-age=31536000, immutable
    ```

    - **避免 CDN 缓存穿透**:为动态资源(如 API 接口)设置 `Cache-Control: no-cache`,防止节点错误缓存动态内容。
    - **选择合适的 CDN 服务商**:优先选择节点覆盖广、性能稳定的服务商(如 Cloudflare、阿里云 CDN、腾讯云 CDN 等)。

    ### 四、其他网络层面优化手段

    #### 1. 压缩传输数据(减少传输体积)

    - **启用 Gzip/Brotli 压缩**:服务器对 JS、CSS、HTML 等文本资源进行压缩(Brotli 比 Gzip 压缩率高 15%-20%),减少传输字节数。
    Nginx 启用 Brotli 示例:

    ```nginx
    brotli on;
    brotli_types text/css application/javascript text/html; # 压缩指定类型
    ```

    - **图片/视频压缩**:使用 WebP/AVIF 格式(比 JPG/PNG 小 25%-50%),视频用 H.265 编码,减少媒体资源传输体积。

    #### 2. 优化 HTTP 缓存(减少重复请求)

    - **强缓存**:通过 `Cache-Control: max-age=xxx` 或 `Expires` 让浏览器直接从本地缓存读取资源,不发起请求(适合长期不变的资源,如库文件)。
    - **协商缓存**:资源过期后,浏览器发送请求带 `ETag` 或 `Last-Modified` 头,服务器判断资源是否变化,未变化返回 304 不传输数据(适合偶尔更新的资源)。

    #### 3. 预连接与预加载(提前建立连接/加载资源)

    - **预连接(preconnect)**:提前与第三方域名建立 TCP + TLS 连接,减少后续请求的握手时间(如 CDN 域名、统计域名):

    ```html
    <link rel="preconnect" href="https://cdn.example.com" crossorigin>
    ```

    - **预加载(preload)**:强制浏览器优先加载关键资源(如首屏 JS、LCP 图片),避免被非关键资源阻塞:

    ```html
    <link rel="preload" as="image" href="hero.webp" fetchpriority="high">
    ```

    ### 总结

    网络层面优化的核心是**“少请求、快传输、近获取”**:

    - 减少请求数量:通过资源合并、按需加载降低连接成本;
    - 提升传输效率:用 HTTP/2 多路复用、压缩传输、HTTP 缓存减少传输时间;
    - 缩短获取距离:通过 CDN 让用户从最近节点获取资源。

    这些手段结合使用,可显著降低资源加载延迟,提升页面打开速度和用户体验。

    ## 5.什么是资源压缩?如何压缩 HTML、CSS、JavaScript 文件?(如使用 webpack 插件、在线工具)

    ### 一、资源压缩的定义

    资源压缩是指通过**去除冗余数据、简化代码格式、优化语法结构**等方式,减小 HTML、CSS、JavaScript 等前端资源的文件体积,从而**减少网络传输时间、降低服务器带宽消耗、提升页面加载速度**的优化手段。

    压缩不会改变资源的功能,仅通过以下方式减小体积:

    - 移除注释、空格、换行符等非功能性字符;
    - 简化变量名(如将 `userName` 改为 `a`);
    - 合并重复代码或规则;
    - 采用更简洁的语法(如 CSS 简写、JS 逻辑简化)。

    ### 二、HTML、CSS、JavaScript 的压缩方法

    根据开发场景(本地构建或临时处理),可通过**构建工具插件**(适合项目化开发)或**在线工具**(适合临时压缩)实现:

    #### 1. HTML 压缩

    HTML 压缩主要去除注释、多余空格、空行,合并相邻标签,简化属性(如移除引号)等。

    ##### (1)webpack 插件:`html-webpack-plugin` + `html-minifier-terser`

    - **原理**:`html-webpack-plugin` 用于生成 HTML 文件,配合 `html-minifier-terser` 对输出的 HTML 进行压缩。
    - **配置示例**:

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

    module.exports = {
    plugins: [
    new HtmlWebpackPlugin({
    template: './src/index.html', // 源 HTML
    filename: 'index.html', // 输出文件名
    minify: { // 压缩配置
    collapseWhitespace: true, // 去除空格
    removeComments: true, // 去除注释
    removeRedundantAttributes: true, // 去除冗余属性(如 input 的 type="text")
    removeScriptTypeAttributes: true, // 去除 script 的 type="text/javascript"
    removeStyleLinkTypeAttributes: true, // 去除 link 的 type="text/css"
    useShortDoctype: true // 使用短文档声明(<!DOCTYPE html>
    }
    })
    ]
    };
    ```

    ##### (2)其他工具

    - **在线工具**:
    - [HTML Minifier](https://html-minifier.com/)(可自定义压缩规则);
    - [Minify HTML](https://www.toptal.com/developers/html-minifier)(简单直观,支持一键压缩)。
    - **Gulp 插件**:`gulp-htmlmin`(配置类似 webpack,适合 Gulp 构建流)。

    #### 2. CSS 压缩

    CSS 压缩主要去除注释、空格,合并重复选择器,简写属性(如 `margin: 10px 10px 10px 10px` → `margin: 10px`),移除无效规则等。

    ##### (1)webpack 插件:`css-minimizer-webpack-plugin`

    - **原理**:在 webpack 构建时压缩 CSS,支持对 `mini-css-extract-plugin` 提取的 CSS 进行压缩,兼容 CSS 嵌套、媒体查询等语法。
    - **配置示例**:

    ```javascript
    // webpack.config.js
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

    module.exports = {
    module: {
    rules: [
    {
    test: /\.css$/,
    use: [MiniCssExtractPlugin.loader, 'css-loader'] // 提取 CSS 到单独文件
    }
    ]
    },
    plugins: [new MiniCssExtractPlugin()], // 提取 CSS
    optimization: {
    minimizer: [
    new CssMinimizerPlugin({ // 压缩 CSS
    minimizerOptions: {
    preset: [
    'default',
    {
    discardComments: { removeAll: true } // 移除所有注释
    }
    ]
    }
    })
    ]
    }
    };
    ```

    ##### (2)其他工具

    - **在线工具**:
    - [CSS Minifier](https://cssminifier.com/)(支持压缩和格式化);
    - [Clean CSS](https://cleancss.com/)(高级压缩,可配置保留特定规则)。
    - **PostCSS 插件**:`cssnano`(通过 PostCSS 集成,可在 webpack、Gulp 等工具中使用,压缩能力强)。

    #### 3. JavaScript 压缩

    JavaScript 压缩(又称“混淆压缩”)不仅去除冗余字符,还会**混淆变量名**(降低可读性)、删除未使用代码(tree-shaking)、简化逻辑(如 `if (a === true)` → `if (a)`)等。

    ##### (1)webpack 插件:`terser-webpack-plugin`

    - **原理**:webpack 生产模式默认集成此插件,基于 Terser(目前最流行的 JS 压缩工具),支持 ES6+ 语法,压缩率高于传统的 UglifyJS。
    - **配置示例**:

    ```javascript
    // webpack.config.js
    const TerserPlugin = require('terser-webpack-plugin');

    module.exports = {
    optimization: {
    minimizer: [
    new TerserPlugin({ // 压缩 JS
    terserOptions: {
    compress: {
    drop_console: true, // 移除 console 语句(生产环境常用)
    drop_debugger: true // 移除 debugger 语句
    },
    mangle: true, // 混淆变量名(默认开启)
    output: {
    comments: false // 移除注释
    }
    }
    })
    ]
    }
    };
    ```

    ##### (2)其他工具

    - **在线工具**:
    - [Terser Online](https://try.terser.org/)(Terser 官方在线工具,支持自定义配置);
    - [JSCompress](https://jscompress.com/)(简单易用,支持混淆和压缩)。
    - **Rollup 插件**:`@rollup/plugin-terser`(适合 Rollup 构建的项目,压缩效果与 webpack 一致)。

    ### 三、压缩注意事项

    1. **保留关键信息**:
    - HTML 压缩避免移除必要空格(如文本间的空格可能影响布局);
    - JS 压缩时通过 `/*@__PURE__*/` 标记纯函数,避免 tree-shaking 误删;
    - 生产环境可移除 `console`/`debugger`,但需保留错误处理逻辑。

    2. **测试压缩后资源**:
    压缩可能导致意外问题(如 CSS 简写兼容性、JS 变量名冲突),需在压缩后测试页面功能是否正常。

    3. **结合缓存策略**:
    压缩后的文件建议添加**内容哈希**(如 `app.8f3d.js`),配合 HTTP 缓存(`Cache-Control`),避免用户缓存旧版本。

    ### 总结

    资源压缩是前端性能优化的基础手段,通过 webpack 等构建工具的插件可实现自动化压缩(适合项目开发),在线工具则适合临时处理单个文件。核心目标是在不影响功能的前提下,最大限度减小文件体积,提升加载速度。

    ## 6.什么是资源合并?资源合并的优势和注意事项是什么?

    ### 一、资源合并的定义

    资源合并是指将前端项目中**多个同类型的小资源文件**(如多个 JavaScript 文件、多个 CSS 文件),通过构建工具(如 webpack、Rollup、Gulp)或手动工具合并为**一个或少数几个大资源文件**的优化手段。

    核心目标是减少浏览器发起的 HTTP 请求数量——例如将 `header.js`、`utils.js`、`main.js` 3 个 JS 文件合并为 `bundle.js`,将 `reset.css`、`theme.css`、`component.css` 3 个 CSS 文件合并为 `style.css`,避免浏览器对多个小文件的重复请求。

    常见的合并工具:webpack(通过 `entry` 配置和 `splitChunks` 插件)、Rollup(通过 `input` 配置)、Gulp(通过 `gulp-concat` 插件)。

    ### 二、资源合并的核心优势

    资源合并的价值主要体现在**网络请求优化**和**工程化管理**两方面,尤其在 HTTP/1.1 环境下效果显著:

    #### 1. 减少 HTTP 请求数量,降低网络开销

    - 每个 HTTP 请求都需经历 **DNS 解析→TCP 握手(HTTPS 还需 TLS 握手)→数据传输→连接关闭** 的流程,单次请求的“握手成本”通常占总耗时的 30%~50%;
    - HTTP/1.1 对同一域名的**并行请求数有限制**(通常 6~8 个),多个小资源会排队等待请求,导致“队头阻塞”;
    - 合并后,原本需要 5 次请求的 5 个小 JS 文件,只需 1 次请求即可加载,大幅减少握手开销和排队时间。

    #### 2. 减少资源头部冗余,降低传输体积

    - 每个独立资源文件在传输时,都会携带重复的 HTTP 头信息(如 `Content-Type`、`Cache-Control`、`User-Agent`)和文件元数据(如文件大小、修改时间);
    - 合并后,多个资源共享一套 HTTP 头,冗余数据减少,进一步降低总传输体积(例如 5 个 JS 文件合并后,HTTP 头从 5 套减少为 1 套)。

    #### 3. 简化缓存管理,提升缓存命中率

    - 合并后的资源可统一设置缓存策略(如 `Cache-Control: max-age=31536000`),避免对多个小资源单独配置缓存;
    - 结合“内容哈希”(如 `bundle.8f3d.js`),只有当合并资源的内容变化时,哈希值才会更新,用户无需重新缓存未修改的内容(配合拆分策略,效果更优)。

    #### 4. 降低工程化管理成本

    - 开发时可按功能拆分小文件(如 `utils/format.js`、`utils/validate.js`),便于维护;
    - 构建时自动合并为少数文件,减少部署时的文件数量,简化 CI/CD 流程。

    ### 三、资源合并的注意事项

    资源合并并非“越多越好”,不当合并可能导致性能反降,需注意以下问题:

    #### 1. 控制合并后的资源体积,避免“单次加载过载”

    - 若将所有 JS/CSS 合并为一个超大文件(如 5MB 以上),会导致**首屏加载时间过长**(尤其在弱网环境下),反而影响用户体验;
    - 解决方案:按“路由/页面”拆分合并(如首屏资源合并为 `main.bundle.js`,“个人中心”路由资源合并为 `user.bundle.js`),配合**按需加载**(路由懒加载),仅加载当前页面所需资源。

    #### 2. 避免“缓存雪崩”,拆分“稳定资源”与“高频更新资源”

    - 若将“长期不变的第三方库”(如 Vue、React)与“频繁更新的业务代码”合并为一个文件,业务代码修改时,整个合并文件的哈希值会变化,导致第三方库的缓存失效(用户需重新下载);
    - 解决方案:通过构建工具拆分(如 webpack 的 `splitChunks`),将第三方库单独合并为 `vendor.bundle.js`,业务代码合并为 `app.bundle.js`,两者独立缓存,互不影响:

    ```javascript
    // webpack.config.js 示例:拆分第三方库
    module.exports = {
    optimization: {
    splitChunks: {
    chunks: 'all',
    cacheGroups: {
    vendor: {
    test: /[\\/]node_modules[\\/]/, // 匹配第三方库
    name: 'vendor', // 合并为 vendor.bundle.js
    priority: 10 // 优先拆分
    }
    }
    }
    }
    };
    ```

    #### 3. 开发环境不建议合并,避免调试困难

    - 合并后的资源会混淆代码结构(如变量名合并、代码顺序调整),开发时若遇到错误,难以定位到原始文件和行号;
    - 解决方案:**开发环境禁用合并**,保持文件独立,通过 `source map` 定位错误;生产环境再启用合并,同时生成 `source map`(用于线上问题调试)。

    #### 4. 不同类型资源不建议合并,避免解析错误

    - 禁止将 JS 与 CSS 合并为一个文件(浏览器对 JS/CSS 的解析引擎不同,合并后会导致解析失败);
    - 图片、字体等二进制资源也不建议与文本资源(JS/CSS)合并,应通过 `CSS Sprites`(图片合并)或 `Data URI`(极小资源)单独处理。

    #### 5. HTTP/2 环境下,减少过度合并

    - HTTP/2 支持**多路复用**(同一 TCP 连接可并行传输多个请求,无队头阻塞),多个小资源的请求开销大幅降低,过度合并的必要性减弱;
    - 解决方案:HTTP/2 环境下,可适当减少合并粒度(如保留部分小资源),避免合并导致的“单次加载体积过大”,结合“预加载(preload)”优化关键资源加载。

    ### 总结

    资源合并是前端性能优化的基础手段,核心价值是“减少 HTTP 请求、降低网络开销”,但需平衡“合并粒度”与“加载效率”:

    - 合并原则:同类型、同页面/路由的资源合并,稳定资源与高频更新资源拆分;
    - 配合策略:结合按需加载、缓存拆分、HTTP/2 特性,避免过度合并导致的性能反降,最终实现“加载快、缓存优、易维护”的目标。

    ## 7.什么是图片优化?图片优化的方式有哪些(如选择合适格式、压缩图片、懒加载、WebP)?

    ### 一、图片优化的定义

    图片优化是针对网页中的图片资源,通过**格式选择、体积压缩、加载策略调整**等手段,在保证视觉质量可接受的前提下,**减小图片文件体积、加快加载速度、降低带宽消耗**的优化过程。

    图片是网页资源体积的主要组成部分(通常占页面总资源的50%以上),优化图片可显著提升页面加载速度(尤其是首屏加载)、减少用户等待时间,是前端性能优化的核心环节。

    ### 二、图片优化的主要方式

    #### 1. 选择合适的图片格式(核心优化)

    不同图片格式的压缩算法和适用场景不同,选择匹配场景的格式可从源头减少体积:

    | 格式 | 特点(压缩算法/支持特性) | 适用场景 | 体积优势(相对JPEG) |
    |------------|---------------------------------------------------|-------------------------------------------|---------------------|
    | **JPEG** | 有损压缩,支持24位真彩色,不支持透明 | 照片、复杂色彩图像(如风景、人像) | 基准(100%) |
    | **PNG-8** | 无损压缩,8位色(256色),支持透明(1位透明) | 简单图标、LOGO、低色彩图形 | 比JPEG小(简单图形) |
    | **PNG-24** | 无损压缩,24位真彩色,支持半透明(alpha通道) | 需要高精度透明的图像(如渐变透明图标) | 较大(不适合大图片) |
    | **WebP** | 同时支持有损/无损压缩,支持透明和动画,谷歌开发 | 几乎所有场景(照片、图标、透明图像) | 比JPEG小25%-35%,比PNG小40% |
    | **AVIF** | 基于AV1编码,压缩率更高,支持透明和动画,新一代格式 | 对体积敏感的场景(如移动端、弱网环境) | 比WebP小20%-30% |
    | **SVG** | 矢量格式,基于XML描述,缩放不失真 | 图标、LOGO、简单图形(如线条、几何图形) | 体积极小(复杂图形可能变大) |

    #### 2. 压缩图片体积(减小传输大小)

    即使格式合适,原始图片体积仍可能过大,需通过压缩进一步减小体积:

    - **有损压缩**:牺牲少量视觉质量换取大幅体积减小(适合照片等对细节敏感度低的场景)。
    - 工具:
    - 在线工具:[TinyPNG](https://tinypng.com/)(支持PNG/JPEG/WebP)、[Squoosh](https://squoosh.app/)(可手动调整压缩率);
    - 后端工具:[Sharp](https://sharp.pixelplumbing.com/)(Node.js库,批量处理)、[ImageMagick](https://imagemagick.org/)(跨平台命令行工具)。

    - **无损压缩**:不损失图片质量,仅去除元数据(如拍摄时间、相机型号)和冗余数据(适合需要精确显示的图片,如设计稿、图表)。
    - 工具:[OptiPNG](http://optipng.sourceforge.net/)(PNG无损压缩)、[JPEGmini](https://www.jpegmini.com/)(JPEG无损压缩)。

    #### 3. 懒加载(延迟非首屏图片加载)

    默认情况下,浏览器会加载页面中所有图片,包括用户暂时看不到的非首屏图片,浪费带宽和加载时间。懒加载通过“**只加载视口内的图片,滚动到可视区域再加载其他图片**”减少初始加载压力。

    - **实现方式**:
    - 原生属性(推荐,现代浏览器支持):为图片添加 `loading="lazy"` 属性:

    ```html
    <img src="non-critical.jpg" loading="lazy" alt="非首屏图片">
    ```

    - JavaScript 实现(兼容旧浏览器):用 `IntersectionObserver` 监听图片是否进入视口,动态设置 `src` 或 `srcset`:

    ```javascript
    // 监听图片进入视口后加载
    const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    const img = entry.target;
    img.src = img.dataset.src; // 从data-src读取真实地址
    observer.unobserve(img); // 停止监听
    }
    });
    });

    // 对所有带data-src的图片应用懒加载
    document.querySelectorAll('img[data-src]').forEach(img => {
    observer.observe(img);
    });
    ```

    - 适用场景:长列表(如商品列表)、含大量图片的页面(如相册、资讯详情)。

    #### 4. 响应式图片(适配不同设备尺寸)

    不同设备的屏幕尺寸和分辨率差异大(如手机、平板、桌面端),加载同一尺寸的图片会导致:

    - 小屏幕设备加载过大图片(浪费带宽);
    - 大屏幕设备加载过小图片(模糊失真)。

    响应式图片通过 `srcset` + `sizes` 或 `<picture>` 标签,让浏览器根据设备条件自动选择合适尺寸的图片:

    - **`srcset` + `sizes`**(推荐,简洁):

    ```html
    <!-- 浏览器根据屏幕宽度自动选择最匹配的图片 -->
    <img
    src="image-400.jpg" <!-- fallback:不支持srcset的浏览器 -->
    srcset="image-400.jpg 400w, <!-- 400px宽的图片 -->
    image-800.jpg 800w, <!-- 800px宽的图片 -->
    image-1200.jpg 1200w" <!-- 1200px宽的图片 -->
    sizes="(max-width: 600px) 400px, <!-- 屏幕≤600px时,图片显示400px宽 -->
    (max-width: 1000px) 800px, <!-- 屏幕≤1000px时,图片显示800px宽 -->
    1200px" <!-- 其他情况显示1200px宽 -->
    alt="响应式图片示例"
    >
    ```

    - **`<picture>` 标签**(用于格式/尺寸双重适配):

    ```html
    <!-- 优先加载AVIF格式(压缩率最高),不支持则降级到WebP,最后到JPEG -->
    <picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <img src="image.jpg" alt="图片示例">
    </picture>
    ```

    #### 5. 预加载关键图片(提升首屏核心图片加载速度)

    首屏的核心图片(如LCP元素:主视觉图、大标题配图)若加载延迟,会直接影响LCP指标。通过 `link rel="preload"` 强制浏览器**优先加载关键图片**,避免被其他资源阻塞:

    ```html
    <!-- 预加载首屏主图(LCP元素) -->
    <link rel="preload" as="image" href="hero.webp" fetchpriority="high">
    ```

    - `as="image"` 告诉浏览器预加载的是图片资源;
    - `fetchpriority="high"` 标记为高优先级,优先于普通图片加载。

    #### 6. 其他优化手段

    - **移除图片元数据**:原始图片可能包含拍摄设备、位置等元数据(非视觉必要),压缩时可删除(如用Sharp的 `withMetadata(false)` 配置);
    - **使用CDN加速**:将图片部署到CDN,用户从最近的节点加载,减少网络延迟;
    - **用SVG替代简单图标**:SVG是矢量图,体积小、缩放不失真,适合图标、LOGO等简单图形(如用 `<svg>` 直接嵌入HTML,避免额外请求)。

    ### 三、总结

    图片优化的核心是“**在视觉质量与体积之间找平衡**”,实际开发中需结合场景综合运用多种策略:

    - 格式选择:优先用WebP/AVIF(现代浏览器),兼容场景降级到JPEG/PNG;
    - 体积控制:通过有损/无损压缩减小体积,避免“大图片小显示”;
    - 加载策略:首屏关键图预加载,非首屏图懒加载,不同设备用响应式图片适配。

    这些手段可显著降低图片加载时间,提升页面性能和用户体验。

    ## 8.不同图片格式(JPG、PNG、GIF、WebP、AVIF)的特点是什么?使用场景分别是什么?

    不同图片格式的核心差异体现在 **压缩方式(有损/无损)、特性支持(透明/动画)、体积大小、兼容性** 四个维度,这些差异直接决定了它们的适用场景。以下是 JPG(JPEG)、PNG、GIF、WebP、AVIF 的详细对比:

    ### 一、各图片格式的特点与使用场景(表格对比)

    | 格式 | 核心特点 | 优势 | 劣势 | 最佳使用场景 |
    |--------|--------------------------------------------------------------------------|---------------------------------------|---------------------------------------|-----------------------------------------------|
    | JPG | 1. **有损压缩**(通过丢弃部分色彩细节减小体积)<br>2. 不支持透明/动画<br>3. 支持 24 位真彩色(约 1677 万色) | 体积小(同视觉质量下比 PNG 小 50%-70%)<br>色彩表现丰富,适合复杂图像 | 压缩会损失细节(放大后模糊)<br>无透明/动画 | 1. 照片(风景、人像、产品图)<br>2. 复杂插图/ banner(色彩多、细节要求不极致)<br>3. 背景图(无透明需求) |
    | PNG | 1. **无损压缩**(不损失细节,可还原原始图像)<br>2. 支持透明(PNG-8 1位透明,PNG-24 alpha 半透明)<br>3. 无动画支持(静态) | 画质无损(边缘清晰)<br>透明效果灵活(半透明/全透明) | 体积大(同尺寸下比 JPG 大 2-3 倍)<br>不支持动画 | 1. 图标/ LOGO(需清晰边缘、透明背景)<br>2. 截图(尤其是带文字/代码的截图,避免模糊)<br>3. UI 组件(如按钮、卡片背景,需半透明) |
    | GIF | 1. **无损压缩**<br>2. 支持简单动画(多帧静态图循环)<br>3. 仅支持 8 位色(256 种颜色)<br>4. 支持 1 位透明(仅“全透明/不透明”) | 支持动画(无需 JS 控制)<br>体积比 PNG 小(简单图像) | 色彩少(复杂图像会模糊)<br>动画帧数/复杂度有限 | 1. 简单动画(加载动图、表情包、按钮hover动画)<br>2. 低色彩静态图(如像素风格图标) |
    | WebP | 1. 支持 **有损/无损压缩**(可按需选择)<br>2. 支持透明(alpha 通道,比 PNG 体积小 40%)<br>3. 支持动画(比 GIF 体积小 50%)<br>4. 谷歌开发,现代浏览器兼容 | 体积极致小(有损比 JPG 小 25%-35%,无损比 PNG 小 40%)<br>特性全面(透明+动画) | 兼容性:不支持 IE/ 旧版 Safari(<14.1) | 1. 替代 JPG:照片、banner(体积更小,画质接近)<br>2. 替代 PNG:图标、截图(透明+无损,体积小)<br>3. 替代 GIF:复杂动画(如短视频缩略动图) |
    | AVIF | 1. 基于 AV1 编码,支持 **有损/无损压缩**<br>2. 支持透明/动画(比 WebP 更流畅)<br>3. 体积比 WebP 小 20%-30%<br>4. 支持 HDR(高动态范围,色彩更细腻) | 压缩率最高(同画质下体积最小)<br>支持 HDR(适合高质量图像) | 兼容性较差(仅支持 Chrome 85+/Safari 16+/Firefox 93+) | 1. 体积敏感场景(移动端弱网、流量限制场景)<br>2. 高质量图像(HDR 照片、4K 插图)<br>3. 未来主流格式过渡(需做好降级兼容) |

    ### 二、关键格式的核心差异补充(避免误用)

    #### 1. JPG vs PNG:选“色彩”还是“清晰透明”?

    - 当图片 **色彩丰富但无透明需求**(如产品照片):选 JPG(体积小,视觉效果足够);
    - 当图片 **需清晰边缘/透明背景**(如图标、截图):选 PNG(无损不模糊,透明效果灵活);
    - 误区:用 JPG 做图标(会导致边缘模糊、文字锯齿),用 PNG 做照片(体积过大,加载慢)。

    #### 2. GIF vs WebP 动画:选“兼容性”还是“体积”?

    - 若需兼容 **IE/旧浏览器**:用 GIF(虽然色彩少,但无兼容问题);
    - 若仅支持现代浏览器:用 WebP 动画(体积比 GIF 小 50%,支持更多色彩和帧数);
    - 示例:微信公众号封面动图可用 WebP(现代浏览器支持),老系统后台加载动图可用 GIF。

    #### 3. WebP vs AVIF:选“兼容性”还是“极致压缩”?

    - 目前 **主流项目首选 WebP**(兼容性覆盖 95%+ 浏览器,压缩率足够);
    - 若项目是 **移动端 App/ 高流量平台**(对带宽敏感):可尝试 AVIF,同时用 `<picture>` 标签降级到 WebP/JPG:

    ```html
    <picture>
    <source srcset="image.avif" type="image/avif"> <!-- 优先 AVIF -->
    <source srcset="image.webp" type="image/webp"> <!-- 降级 WebP -->
    <img src="image.jpg" alt="示例图"> <!-- 最终降级 JPG -->
    </picture>

三、格式选择总结(开发决策指南)

  1. 日常开发优先用 WebP:覆盖 95%+ 现代浏览器,兼顾体积、画质、特性(透明/动画),是当前性价比最高的选择;
  2. 需兼容 IE/ 旧 Safari:用 JPG(照片)+ PNG(图标)+ GIF(动画)组合;
  3. 体积敏感/高质量需求:用 AVIF(如移动端弱网场景、4K 图片),需做好降级;
  4. 简单动画/表情包:WebP 动画(现代)或 GIF(兼容);
  5. UI 组件/截图:PNG(无损透明)或 WebP 无损(体积更小)。

通过匹配格式特性与业务场景,可在“视觉质量”和“加载性能”之间找到最佳平衡,是图片优化的核心第一步。

9.什么是图片懒加载?如何实现图片懒加载(原生 loading 属性、IntersectionObserver)?

一、图片懒加载的定义

图片懒加载(Lazy Loading)是一种按需加载图片的优化策略:页面初始化时,仅加载视口内(用户当前能看到的屏幕区域) 的图片;当用户滚动页面,使非首屏图片进入视口时,再动态加载这些图片。

核心目的是:

  • 减少初始 HTTP 请求数量,降低首屏加载时间(尤其适合含大量图片的页面,如相册、商品列表);
  • 节省用户带宽(避免加载用户可能永远看不到的图片);
  • 优化 CLS(累积布局偏移):配合占位符可避免图片加载后撑开布局。

二、图片懒加载的两种核心实现方式

1. 原生 loading="lazy" 属性(推荐,简单高效)

现代浏览器(Chrome 77+、Firefox 75+、Safari 15.4+)原生支持图片懒加载,无需编写 JS,仅需给 <img><iframe> 标签添加 loading="lazy" 属性即可。

(1)原理

浏览器会自动检测图片是否进入视口,进入时才触发加载;未进入时,仅加载图片的占位区域(需提前设置宽高避免布局偏移)。

(2)代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
<!-- 基础用法:非首屏图片添加 loading="lazy" -->
<img
src="product-1.jpg"
loading="lazy" <!-- 原生懒加载开关 -->
width="800" <!-- 提前设置宽高,避免布局偏移 -->
height="600"
alt="商品图片1"
>

<!-- 配合响应式图片(srcset + sizes) -->
<img
srcset="product-400.jpg 400w, product-800.jpg 800w"
sizes="(max-width: 600px) 400px, 800px"
src="product-400.jpg"
loading="lazy"
alt="响应式商品图片"
>
```

##### (3)注意事项

- **兼容性**:不支持 IE 和旧版浏览器(如 Safari <15.4),需降级(可结合 IntersectionObserver 做兼容);
- **占位符**:必须提前设置 `width`/`height` 或通过 CSS 固定 `aspect-ratio`(如 `aspect-ratio: 4/3`),避免图片加载后布局偏移;
- **首屏图片不建议懒加载**:首屏核心图片(如 LCP 元素)若懒加载,会延迟 LCP 指标,影响性能评分。

#### 2. IntersectionObserver API(灵活可控,兼容更广)

`IntersectionObserver` 是浏览器提供的 API,用于监听“目标元素是否进入视口”,可自定义懒加载逻辑(如加载中状态、加载失败处理),兼容性优于原生属性(支持 Chrome 51+、Firefox 55+、Safari 12.1+)。

##### (1)原理

1. 初始化 `IntersectionObserver` 实例,定义“元素进入视口时的回调函数”;
2. 给非首屏图片添加 `data-src`(存储真实图片地址),初始 `src` 设为占位图(如骨架屏、1x1 透明像素);
3. 用 `observer.observe()` 监听图片元素;
4. 当图片进入视口,回调函数触发:将 `data-src` 赋值给 `src`,加载真实图片,加载完成后停止监听(避免重复触发)。

##### (2)代码示例

```html
<!-- HTML:非首屏图片用 data-src 存真实地址,src 设占位图 -->
<img
class="lazy"
data-src="product-2.jpg" <!-- 真实图片地址 -->
src="placeholder.png" <!-- 占位图(骨架屏/透明像素) -->
width="800"
height="600"
alt="商品图片2"
>
<img
class="lazy"
data-src="product-3.jpg"
src="placeholder.png"
width="800"
height="600"
alt="商品图片3"
>
```

```javascript
// JavaScript:用 IntersectionObserver 实现懒加载
document.addEventListener('DOMContentLoaded', () => {
// 1. 初始化观察者:定义进入视口的回调
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// 2. 判断元素是否进入视口
if (entry.isIntersecting) {
const img = entry.target;
// 3. 加载真实图片:将 data-src 赋值给 src
if (img.dataset.src) {
img.src = img.dataset.src;
// 加载完成后移除 data-src(避免重复处理)
img.removeAttribute('data-src');
// 4. 停止监听该图片(优化性能)
observer.unobserve(img);
}

// 可选:处理加载失败(设置默认图)
img.onerror = () => {
img.src = 'default-error.jpg';
};

// 可选:添加加载中动画(如淡入)
img.onload = () => {
img.style.opacity = 1; // 加载完成后显示
};
}
});
}, {
// 配置:图片进入视口 10% 时开始加载(提前加载,提升体验)
rootMargin: '0px 0px 200px 0px', // 视口底部向外扩展 200px,提前触发加载
threshold: 0.1
});

// 5. 监听所有带 .lazy 类的图片
document.querySelectorAll('img.lazy').forEach(img => {
observer.observe(img);
});
});
```

```css
/* CSS:设置占位图样式和加载动画 */
img.lazy {
opacity: 0; /* 初始透明,加载完成后设为 1 */
transition: opacity 0.3s ease; /* 淡入动画 */
aspect-ratio: 4/3; /* 固定宽高比,避免布局偏移 */
object-fit: cover; /* 图片自适应容器 */
}
```

##### (3)核心优势

- **自定义逻辑**:可添加加载中动画、加载失败默认图、优先级控制(如首屏附近图片优先加载);
- **提前加载**:通过 `rootMargin` 配置“提前加载距离”(如图片还在视口下方 200px 时就开始加载),避免用户滚动时看到空白;
- **兼容性好**:可通过 polyfill(如 `intersection-observer` 库)兼容 IE11 等旧浏览器。

### 三、两种实现方式的对比与选择

| 维度 | 原生 `loading="lazy"` | IntersectionObserver API |
|--------------|---------------------------------------|---------------------------------------|
| 实现复杂度 | 极低(仅需加属性) | 中等(需编写 JS/CSS) |
| 灵活性 | 低(无自定义逻辑) | 高(支持加载动画、失败处理等) |
| 兼容性 | 现代浏览器(Chrome 77+/Safari 15.4+) | 更广(Chrome 51+/Safari 12.1+,可 polyfill) |
| 优化细节 | 无(依赖浏览器默认行为) | 可提前加载、控制优先级 |

**选择建议**:

- 简单场景(如博客、静态页):优先用原生 `loading="lazy"`,成本低;
- 复杂场景(如电商列表、相册):用 `IntersectionObserver`,支持自定义体验和兼容性处理;
- 极致兼容(需支持 IE):用 `IntersectionObserver + polyfill`,或降级为“滚动监听”(不推荐,性能差)。

### 四、关键优化点(避免踩坑)

1. **必须设置占位符/宽高比**:无论哪种方式,都需固定图片容器尺寸(`width/height` 或 `aspect-ratio`),否则图片加载后会撑开布局,导致 CLS 升高;
2. **首屏图片不懒加载**:首屏核心图片(如 LCP 元素)若懒加载,会延迟 LCP 指标,影响性能评分;
3. **避免过度懒加载**:非首屏但用户可能快速滚动到的图片(如首屏下方 1-2 屏),建议提前加载,避免空白;
4. **测试真实场景**:在弱网环境下测试,确保懒加载触发时机合理,避免用户等待过久。

通过合理的图片懒加载实现,可显著减少初始请求数量和带宽消耗,提升页面加载速度和用户体验。

## 10.什么是字体优化?字体优化的方式有哪些(如字体子集化、字体预加载、fallback 字体)?

### 一、字体优化的定义与核心痛点

字体优化是针对网页中**自定义字体(如思源黑体、Roboto、PingFang SC)** 的加载、渲染过程进行优化,核心目标是解决以下问题,提升用户体验:

1. **FOIT(Flash of Invisible Text,文字不可见闪烁)**:自定义字体加载完成前,浏览器不显示文字(空白),导致用户无法阅读;
2. **FOUT(Flash of Unstyled Text,文字样式闪烁)**:字体加载完成后,从“系统默认字体”切换到“自定义字体”,可能导致文字尺寸、间距变化,引发布局偏移(CLS 升高);
3. **字体文件体积大**:完整的中文字体文件(如思源黑体)可达 10MB+,加载慢(尤其弱网环境),阻塞页面渲染。

简言之,字体优化的核心是:**让自定义字体“加载更快、显示不卡、切换不晃”**。

### 二、字体优化的核心方式

#### 1. 字体子集化(Subsetting):减小字体文件体积

完整字体文件包含大量字符(如中文有 2 万+ 常用字、特殊符号),但网页通常只用到部分字符(如正文常用 3000 字、数字、标点)。**字体子集化**即只保留网页需要的字符,剔除冗余字符,大幅减小文件体积(通常可从 10MB 压缩到 100KB 以内)。

##### (1)实现方法

- **工具选择**:
- 在线工具:[Font Squirrel Webfont Generator](https://www.fontsquirrel.com/tools/webfont-generator)(上传字体,选择“Custom Subsetting”,勾选需要的字符集);
- 本地工具:[glyphhanger](https://github.com/filamentgroup/glyphhanger)(扫描网页 HTML,自动提取用到的字符,生成子集字体);
- 专业工具:FontLab(手动筛选字符,适合精细控制)。
- **常见子集策略**:
- 中文:仅保留“GB2312 常用字”(3755 个)+ 数字 + 标点;
- 英文:仅保留“大小写字母”+ 数字 + 常用标点(无需特殊符号)。

##### (2)示例(glyphhanger 用法)

```bash
# 1. 安装 glyphhanger
npm install -g glyphhanger

# 2. 扫描本地 HTML 文件,提取用到的字符
glyphhanger ./src/**/*.html --files '**/*.css'

# 3. 生成子集字体(以思源黑体为例)
glyphhanger --subset ./fonts/source-han-sans.ttf --formats woff2,woff
```

##### (3)注意事项

- 避免过度子集化:若网页后续新增字符(如用户评论中的生僻字),会导致字体显示异常,需预留必要字符;
- 优先输出 WOFF2 格式(体积最小,兼容性好)。

#### 2. 字体预加载(Preload):提前加载关键字体

自定义字体默认“按需加载”(即浏览器解析到 `font-family` 时才开始加载),可能导致加载延迟。**字体预加载**通过 `<link rel="preload">` 强制浏览器在页面初始化时优先加载“关键字体”(如正文、标题用的字体),避免 FOIT。

##### (1)实现代码

```html
<!-- 预加载关键字体(思源黑体子集,WOFF2 格式) -->
<link
rel="preload"
as="font" <!-- 声明资源类型为字体 -->
href="/fonts/source-han-sans-subset.woff2"
type="font/woff2"
crossorigin <!-- 必须添加:字体属于跨域资源(即使同域也建议加),否则预加载失败 -->
>

<!-- CSS 中定义字体 -->
<style>
@font-face {
font-family: 'Source Han Sans';
src: url('/fonts/source-han-sans-subset.woff2') format('woff2'),
url('/fonts/source-han-sans-subset.woff') format('woff'); /* 降级格式 */
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Source Han Sans', sans-serif; /* 应用自定义字体 */
}
</style>
```

##### (2)注意事项

- 仅预加载“关键字体”:如正文、标题字体,非关键字体(如侧边栏小文字、按钮图标字体)无需预加载,避免阻塞其他资源;
- 控制预加载数量:同一页面预加载字体不超过 2 个,过多会占用带宽,影响首屏加载。

#### 3. 配置 Fallback 字体与 `font-display`:避免 FOIT/FOUT

**Fallback 字体**是自定义字体加载完成前,浏览器临时显示的“系统默认字体”(如 `sans-serif`、`Microsoft YaHei`);**`font-display`** 是 CSS 属性,控制字体加载过程中“文字显示策略”,核心是解决 FOIT 和布局偏移。

##### (1)`font-display` 核心取值(推荐 `swap`)

| 取值 | 行为说明 | 适用场景 |
|------------|--------------------------------------------------------------------------|---------------------------|
| `swap` | 字体加载前显示 Fallback 字体,加载完成后立即替换(无 FOIT,轻微 FOUT) | 正文、标题(优先保证可读性) |
| `fallback` | 字体加载前隐藏文字(最多 100ms),超时后显示 Fallback,加载完成后替换 | 非核心文字(如标签、注释) |
| `optional` | 浏览器自主决定是否加载字体(弱网环境可能不加载,直接用 Fallback) | 非必需的装饰性文字 |

##### (2)实现代码(Fallback + `font-display`)

```css
@font-face {
font-family: 'Source Han Sans';
src: url('/fonts/source-han-sans-subset.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* 关键:避免 FOIT,允许轻微 FOUT */
}

/* 配置合理的 Fallback 字体链,减少切换时的布局偏移 */
body {
/* 自定义字体 → 系统中文字体 → 通用无衬线字体 */
font-family: 'Source Han Sans', 'Microsoft YaHei', 'PingFang SC', sans-serif;
font-size: 16px; /* 固定字体大小,避免切换时尺寸变化 */
line-height: 1.5; /* 固定行高,避免布局偏移 */
}
```

##### (3)关键优化点

- Fallback 字体选择“风格接近”的字体:如自定义字体是无衬线字体,Fallback 也选无衬线(`sans-serif`),减少切换时的视觉差异;
- 固定 `font-size` 和 `line-height`:避免自定义字体与 Fallback 字体的尺寸、行高不同,导致文字换行或布局偏移(CLS 升高)。

#### 4. 选择高效的字体格式:减小传输体积

不同字体格式的压缩率和兼容性差异大,优先选择“体积小、兼容性好”的格式,顺序如下:

| 格式 | 压缩率(体积) | 兼容性(现代浏览器覆盖) | 特点 |
|--------|----------------|--------------------------|--------------------------|
| WOFF2 | 最高(最小) | 95%+(Chrome 36+/Safari 10+/Firefox 39+) | 基于 Brotli 压缩,体积比 WOFF 小 30% |
| WOFF | 较高 | 99%+(IE 9+/所有现代浏览器) | 基于 zlib 压缩,兼容性好 |
| TTF/OTF| 较低(最大) | 100%(所有浏览器) | 原始格式,体积大,仅作降级 |

##### (1)实现代码(多格式降级)

```css
@font-face {
font-family: 'Source Han Sans';
/* 优先加载 WOFF2 → 降级 WOFF → 最后 TTF */
src: url('/fonts/source-han-sans-subset.woff2') format('woff2'),
url('/fonts/source-han-sans-subset.woff') format('woff'),
url('/fonts/source-han-sans-subset.ttf') format('truetype');
font-weight: 400;
font-display: swap;
}
```

#### 5. 异步加载非关键字体:不阻塞首屏

对于“非关键字体”(如页脚文字、广告文字、弹窗文字),无需预加载或优先加载,可通过 **JS 动态加载**,避免影响首屏性能。

##### (1)实现代码(JS 异步加载)

```javascript
// 页面加载完成后,异步加载非关键字体(如广告用的字体)
window.addEventListener('load', () => {
// 1. 创建 style 标签
const style = document.createElement('style');
style.textContent = `
@font-face {
font-family: 'Ad Font';
src: url('/fonts/ad-font-subset.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
.ad-text { font-family: 'Ad Font', sans-serif; }
`;
// 2. 插入到 head 末尾,避免阻塞首屏
document.head.appendChild(style);
});
```

#### 6. 使用变量字体(Variable Fonts):减少字体文件数量

传统字体需为“不同字重(常规/粗体/细体)、不同样式(斜体/正常)”单独生成文件(如 `font-regular.ttf`、`font-bold.ttf`),导致文件数量多、请求多。**变量字体(Variable Fonts)** 将多个字重/样式整合到一个文件中,体积比多个单独文件小 50%+。

##### (1)实现代码

```css
/* 加载变量字体(一个文件包含所有字重) */
@font-face {
font-family: 'Source Han Sans Variable';
src: url('/fonts/source-han-sans-variable.woff2') format('woff2 supports variations'),
url('/fonts/source-han-sans-variable.woff2') format('woff2-variations');
font-weight: 100 900; /* 支持的字重范围(100=细体,900=黑体) */
font-display: swap;
}

/* 应用不同字重,无需加载多个文件 */
body { font-family: 'Source Han Sans Variable', sans-serif; font-weight: 400; } /* 常规 */
h1 { font-weight: 700; } /* 粗体 */
.light-text { font-weight: 300; } /* 细体 */
```

### 三、字体优化的注意事项

1. **避免过度优化**:非自定义字体(仅用系统字体)无需优化;
2. **测试兼容性**:IE 不支持 WOFF2 和 `font-display`,需降级为 WOFF/TTF,并确保 Fallback 字体正常显示;
3. **结合 CDN 加速**:将字体文件部署到 CDN,减少网络延迟(注意配置 `Access-Control-Allow-Origin`,支持跨域预加载);
4. **监控性能**:用 Lighthouse 检测“字体加载时间”和“FOIT 时长”,针对性优化。

### 四、总结

字体优化的核心逻辑是“**减小体积+提前加载+平稳切换**”:

- 体积优化:子集化、选择 WOFF2/变量字体;
- 加载优化:预加载关键字体、异步加载非关键字体;
- 体验优化:配置 Fallback 字体和 `font-display: swap`,避免 FOIT 和布局偏移。

通过以上方式,可在保证自定义字体视觉效果的同时,最大化提升加载速度和用户体验。

## 11.如何优化 CSS 性能?(如减少选择器复杂度、避免使用 @import、CSS 压缩、关键 CSS)

CSS 性能优化的核心目标是**减少浏览器解析与渲染 CSS 的时间**、**降低资源体积**、**避免阻塞页面首次渲染**,最终提升页面加载速度和交互流畅度。以下是关键优化策略,包含原理和具体实现:

### 一、减少选择器复杂度(降低解析成本)

浏览器解析 CSS 时,采用**从右到左的匹配规则**(如 `.box .item span` 会先找所有 `span`,再筛选父级为 `.item` 且祖父级为 `.box` 的元素)。选择器越复杂,匹配耗时越长,尤其在元素数量多的页面(如长列表)中影响显著。

#### 优化方向

1. **避免嵌套过深**(建议不超过 3 层)
- 反例(嵌套过深,匹配链长):

```css
.header .nav .list .item .link { color: #333; }
```

- 正例(简化嵌套,直接定位):

```css
.nav-link { color: #333; } /* 给元素添加独立类名 */
```

2. **减少通配符和属性选择器**
- 通配符(`*`)会匹配页面所有元素,属性选择器(`[type="text"]`)匹配效率低,尽量用类选择器替代:
- 反例:

```css
* { margin: 0; padding: 0; } /* 匹配所有元素,成本高 */
input[type="text"] { border: 1px solid #ccc; }
```

- 正例:

```css
body, div, p, ul, li { margin: 0; padding: 0; } /* 明确指定必要元素 */
.text-input { border: 1px solid #ccc; } /* 用类名替代属性选择器 */
```

3. **避免无意义的限定符**
- 无需给类选择器添加标签限定(如 `div.box`),除非明确需要区分同类别名的不同标签:
- 反例:

```css
div.box { width: 100px; } /* 多余的 div 限定 */
```

- 正例:

```css
.box { width: 100px; }
```

### 二、避免使用 `@import`(优化加载顺序)

`@import` 用于在 CSS 中导入其他 CSS 文件,但存在**加载阻塞问题**:

- 浏览器解析到 `@import` 时,必须暂停当前 CSS 解析,先加载并解析导入的文件,导致后续资源加载延迟;
- `@import` 无法并行加载多个 CSS 文件(link 标签可并行加载),增加总加载时间。

#### 优化方案:用 `<link>` 标签替代 `@import`

- 反例(`@import` 导入):

```css
/* style.css */
@import url("reset.css"); /* 阻塞后续解析 */
@import url("theme.css");
```

- 正例(`<link>` 并行加载):

```html
<link rel="stylesheet" href="reset.css"> <!-- 并行加载 -->
<link rel="stylesheet" href="theme.css"> <!-- 并行加载 -->
<link rel="stylesheet" href="style.css">
```

### 三、CSS 压缩(减小传输体积)

CSS 文件中包含大量空格、换行、注释等冗余信息,压缩可去除这些内容,减小文件体积(通常压缩率 30%~50%),加快传输速度。

#### 实现方式

1. **构建工具压缩**(推荐,自动化处理)
- webpack:使用 `css-minimizer-webpack-plugin`(配合 `mini-css-extract-plugin` 提取 CSS):

```javascript
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
module: {
rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }]
},
plugins: [new MiniCssExtractPlugin()],
optimization: {
minimizer: [new CssMinimizerPlugin()] // 压缩 CSS
}
};
```

- Gulp:使用 `gulp-clean-css` 插件。

2. **在线工具压缩**(临时处理单个文件)
- [CSS Minifier](https://cssminifier.com/)、[Clean CSS](https://cleancss.com/) 等,支持自定义压缩规则(如是否保留注释)。

### 四、提取关键 CSS(Critical CSS,避免首屏渲染阻塞)

**关键 CSS** 是指“首屏渲染必需的 CSS”(如导航、 banner、首屏内容的样式)。若将所有 CSS 合并为一个大文件,浏览器需等待全部加载解析完成才会渲染页面(CSS 会阻塞渲染),导致首屏加载慢。

#### 优化方案

1. **内联关键 CSS 到 HTML**:将首屏必需的 CSS 直接写在 `<head>` 的 `<style>` 标签中,避免额外请求,让浏览器解析 HTML 时即可获取样式,快速渲染首屏。
2. **异步加载非关键 CSS**:非首屏 CSS(如页脚、弹窗样式)通过 `<link rel="preload">` 或动态加载,不阻塞首屏渲染。

#### 实现示例

```html
<head>
<!-- 1. 内联关键 CSS(首屏必需) -->
<style>
/* 仅包含首屏导航、banner、主内容样式 */
.header { height: 60px; background: #fff; }
.banner { width: 100%; height: 300px; }
.main-content { padding: 20px; }
</style>

<!-- 2. 异步加载非关键 CSS(非首屏样式) -->
<link
rel="preload"
href="non-critical.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
>
<!-- 降级处理:不支持 preload 的浏览器 -->
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
</head>
```

#### 提取工具

- [Critical](https://github.com/addyosmani/critical):自动分析页面,提取首屏关键 CSS;
- webpack 插件:`critical-webpack-plugin`,构建时自动内联关键 CSS。

### 五、其他核心优化策略

1. **避免使用昂贵的 CSS 属性**
部分 CSS 属性会触发**重排(Reflow)** 或**重绘(Repaint)**,频繁使用(尤其在动画中)会导致性能损耗:
- 昂贵属性:`box-shadow`、`filter`、`opacity`(低版本浏览器)、`transform: translateZ(0)`(触发 GPU 加速但可能导致内存占用过高);
- 优化:动画中优先使用 `transform` 和 `opacity`(仅触发合成层,不重排重绘):

```css
/* 高效动画:仅触发合成层 */
.box { transition: transform 0.3s; }
.box:hover { transform: translateX(10px); }
```

2. **减少 CSS 文件数量**
多个小 CSS 文件会增加 HTTP 请求,建议合并为少数文件(结合拆分策略:如按路由拆分,避免单个文件过大)。

3. **使用 CSS Containment 隔离渲染**
对独立组件(如弹窗、侧边栏)添加 `contain: layout paint size`,告诉浏览器该元素的渲染不会影响其他部分,减少渲染计算范围:

```css
.modal {
contain: layout paint size; /* 隔离渲染,提升性能 */
position: fixed;
width: 500px;
height: 300px;
}
```

4. **避免使用 `!important`**
`!important` 会破坏 CSS 优先级规则,增加调试难度,且可能导致后续样式覆盖需重复使用 `!important`,增加代码冗余。

### 总结

CSS 性能优化的核心逻辑是:**降低解析成本(简化选择器)、减少加载阻塞(避免 @import、异步加载)、减小体积(压缩)、优先首屏渲染(关键 CSS)**。结合具体场景实施这些策略,可显著提升页面渲染速度和交互流畅度。

## 12.如何优化 JavaScript 性能?(如减少重排重绘、避免阻塞渲染、代码分割、懒加载)

JavaScript 性能优化的核心是**减少主线程负担**(避免阻塞渲染和交互)、**优化资源加载效率**、**提升代码执行效率**,最终实现页面“响应快、加载快、运行稳”。以下是关键优化策略,结合具体场景和实现方式:

### 一、减少重排(Reflow)与重绘(Repaint):优化 DOM 操作

DOM 操作是 JavaScript 性能瓶颈的常见来源,因为浏览器渲染 DOM 时存在“重排”和“重绘”两个耗时过程:

- **重排**:元素布局(位置、尺寸)变化时,浏览器需重新计算所有相关元素的几何位置(如修改 `width`、`top`),开销极大;
- **重绘**:元素样式(如 `color`、`background`)变化但不影响布局时,浏览器仅重新绘制元素外观,开销小于重排,但频繁触发仍会卡顿。

#### 优化策略

1. **批量操作 DOM,减少触发次数**
频繁单独修改 DOM 会多次触发重排,应合并操作后一次性更新:
- 反例(多次触发重排):

```javascript
const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // 每次 append 都触发重排
}
```

- 正例(批量操作,一次重排):

```javascript
const list = document.getElementById('list');
// 1. 用文档片段(DocumentFragment)临时存储节点
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item); // 不触发重排
}
// 2. 一次性添加到 DOM
list.appendChild(fragment); // 仅触发一次重排
```

2. **避免频繁读取/修改布局属性**
浏览器会缓存布局信息(如 `offsetHeight`、`scrollTop`),但**读取布局属性后立即修改**会强制浏览器“刷新缓存”,触发额外重排(称为“强制同步布局”):
- 反例(强制同步布局):

```javascript
const box = document.getElementById('box');
for (let i = 0; i < 100; i++) {
const height = box.offsetHeight; // 读取布局
box.style.height = `${height + 10}px`; // 立即修改,触发重排
}
```

- 正例(先读再改,避免强制同步):

```javascript
const box = document.getElementById('box');
const height = box.offsetHeight; // 先批量读取
for (let i = 0; i < 100; i++) {
box.style.height = `${height + 10 * i}px`; // 再批量修改
}
```

3. **使用“脱离文档流”的方式修改 DOM**
对元素设置 `display: none` 后,其修改不会触发重排(脱离文档流),修改完成后再显示:

```javascript
const box = document.getElementById('box');
box.style.display = 'none'; // 脱离文档流,后续修改不触发重排
box.style.width = '200px';
box.style.height = '200px';
box.style.display = 'block'; // 重新显示,仅触发一次重排
```

4. **用 CSS `transform` 和 `opacity` 实现动画**
这两个属性修改时**仅触发“合成层”更新**(GPU 加速),不触发重排或重绘,是高性能动画的首选:

```css
/* 高效动画:无重排/重绘 */
.box {
transition: transform 0.3s;
}
.box:hover {
transform: translateX(10px); /* 仅触发合成层更新 */
}
```

### 二、避免阻塞渲染:优化主线程执行

JavaScript 运行在浏览器主线程,与渲染(HTML 解析、CSS 计算、布局绘制)共享线程。若 JS 执行时间过长(超过 50ms,称为“长任务”),会阻塞渲染和用户交互,导致页面卡顿、FID(首次输入延迟)升高。

#### 优化策略

1. **拆分长任务,避免主线程阻塞**
将耗时超过 50ms 的计算(如大数据循环、复杂逻辑)拆分成小块,用 `requestIdleCallback` 或 `setTimeout` 分批次执行,给主线程留出处理渲染和交互的时间:

```javascript
// 反例:长任务阻塞主线程
function processLargeData(data) {
for (let i = 0; i < data.length; i++) {
// 复杂计算(假设 data 有 10 万条数据)
}
}

// 正例:拆分任务,利用浏览器空闲时间执行
function processInChunks(data, chunkSize = 100) {
let index = 0;

function processChunk(deadline) {
// 利用浏览器空闲时间(deadline.timeRemaining() > 0)执行
while (index < data.length && deadline.timeRemaining() > 0) {
// 处理当前块(每次 100 条)
processItem(data[index]);
index++;
}

// 未处理完,继续请求下一次空闲时间
if (index < data.length) {
requestIdleCallback(processChunk);
}
}

// 启动任务
requestIdleCallback(processChunk);
}
```

2. **用 Web Workers 处理计算密集型任务**
Web Workers 允许在**独立线程**中执行 JS,不阻塞主线程,适合处理大数据分析、图表渲染、复杂算法等:

```javascript
// 主线程:创建 Worker 并发送数据
const dataWorker = new Worker('data-processor.js');
dataWorker.postMessage(largeDataset); // 发送大数据

// 接收处理结果
dataWorker.onmessage = (e) => {
console.log('处理完成:', e.data);
updateUI(e.data); // 主线程仅负责更新 UI
};

// data-processor.js(Worker 线程):处理计算
self.onmessage = (e) => {
const result = heavyCalculation(e.data); // 密集计算在独立线程
self.postMessage(result); // 发送结果回主线程
};
```

3. **延迟加载非关键 JS,优先保证首屏交互**
非首屏必需的 JS(如统计代码、广告脚本)应延迟加载,避免阻塞首屏渲染:
- 用 `defer` 或 `async` 属性:

```html
<!-- async:加载完成后立即执行(不保证顺序) -->
<script src="analytics.js" async></script>
<!-- defer:加载完成后,等待 DOM 解析完成再执行(保证顺序) -->
<script src="ad.js" defer></script>
```

- 动态导入(适合模块化项目):

```javascript
// 页面加载完成后,再加载非关键模块
window.addEventListener('load', () => {
import('./non-critical-module.js').then((module) => {
module.init();
});
});
```

### 三、代码分割与懒加载:优化资源加载

初始加载时若加载大量无关 JS(如未访问的路由、未触发的功能),会增加加载时间和内存占用。代码分割和懒加载通过“按需加载”减少初始资源体积。

#### 优化策略

1. **路由级代码分割**
用构建工具(如 webpack、Vite)按路由拆分代码,仅加载当前页面所需 JS:
- React 路由懒加载(`React.lazy` + `Suspense`):

```javascript
import { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 懒加载路由组件(仅访问时才加载)
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}> {/* 加载中占位 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
```

- webpack 配置(自动拆分公共库):

```javascript
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 拆分所有类型的 chunk
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/, // 拆分第三方库(如 React、Vue)
name: 'vendors',
chunks: 'all'
}
}
}
}
};
```

2. **组件/功能级懒加载**
对“用户触发后才需要”的功能(如弹窗、下拉菜单),在用户交互时才加载对应代码:

```javascript
// 点击按钮时,才加载弹窗组件
document.getElementById('open-modal').addEventListener('click', async () => {
// 动态导入弹窗模块
const { Modal } = await import('./modal.js');
new Modal().show(); // 显示弹窗
});
```

### 四、其他核心优化策略

1. **减少全局变量和闭包**
全局变量会常驻内存(直到页面关闭),闭包可能导致变量无法被垃圾回收,造成内存泄漏。优化方式:
- 用模块作用域(`import`/`export`)替代全局变量;
- 避免不必要的闭包,及时解除引用(如 `obj = null`)。

2. **优化事件监听:防抖(Debounce)与节流(Throttle)**
对 `resize`、`scroll`、`input` 等高频触发事件,用防抖/节流限制执行频率:
- 防抖(触发后延迟 n 秒执行,再次触发则重置延迟):适合搜索输入联想;
- 节流(n 秒内仅执行一次):适合滚动加载、窗口 resize。

```javascript
// 防抖函数
function debounce(fn, delay = 300) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}

// 搜索输入联想(防抖)
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
fetchSuggestions(e.target.value);
}, 300));
```

3. **避免使用 `eval` 和 `with`**
- `eval` 会将字符串转为代码执行,破坏作用域,降低性能(浏览器无法优化);
- `with` 会改变作用域链,增加变量查找时间。
建议用函数参数或对象解构替代。

4. **使用高效的选择器和 API**
- 优先用 `getElementById`、`querySelector`(性能优于 `getElementsByClassName`);
- 遍历 DOM 时用 `for` 循环或 `forEach`(优于 `for...in`);
- 用 `textContent` 替代 `innerHTML`(避免 HTML 解析,更安全)。

### 总结

JavaScript 性能优化的核心逻辑是:

- **DOM 操作**:减少重排重绘(批量操作、避免强制同步布局);
- **主线程**:拆分长任务、用 Web Workers 分担计算、延迟加载非关键 JS;
- **资源加载**:代码分割(路由/组件级)、按需懒加载;
- **代码质量**:减少内存泄漏、优化事件监听、避免低效语法。

通过以上策略,可显著提升页面响应速度和运行稳定性,改善用户体验。

## 13.什么是关键渲染路径?如何优化关键渲染路径(如优先加载关键资源、减少渲染阻塞)?

### 一、关键渲染路径(Critical Rendering Path)的定义

关键渲染路径是指**浏览器将 HTML、CSS、JavaScript 等资源转换为屏幕上可见像素的核心流程**,直接决定了页面的**首屏加载时间(FCP)** 和**首次有效绘制(FMP)**,是前端性能优化的核心靶点。

其本质是浏览器完成“从资源到像素”的最小必要步骤,任何步骤的延迟都会导致首屏渲染变慢,影响用户体验(如用户看到空白页时间过长)。

### 二、关键渲染路径的核心流程(5个步骤)

浏览器需依次完成以下步骤,且**前一步未完成时,后一步无法开始**(存在“阻塞关系”):

1. **解析 HTML 生成 DOM 树(DOM Construction)**
- 浏览器读取 HTML 文件,按顺序解析标签(如 `<div>`、`<p>`),生成 **DOM 树**(Document Object Model)—— 描述页面的结构和内容(不含样式)。
- 阻塞点:若遇到 `<script>` 标签(同步 JS),会暂停 HTML 解析,先执行 JS(因 JS 可能修改 DOM),导致 DOM 树构建延迟。

2. **解析 CSS 生成 CSSOM 树(CSSOM Construction)**
- 浏览器读取 CSS 文件(内联 `<style>` 或外部 `<link>`),解析样式规则,生成 **CSSOM 树**(CSS Object Model)—— 描述页面所有元素的样式(如颜色、尺寸)。
- 阻塞点:CSSOM 会**阻塞渲染树生成**(无样式则无法确定元素如何显示),且同步 JS 会等待 CSSOM 构建完成后再执行(因 JS 可能读取/修改样式)。

3. **结合 DOM + CSSOM 生成渲染树(Render Tree Construction)**
- 浏览器遍历 DOM 树,为每个可见元素(如排除 `display: none` 的元素)匹配对应的 CSSOM 样式,生成 **渲染树**—— 仅包含“需要显示的元素”及其样式。
- 注意:渲染树不包含不可见元素(如 `<head>` 内容、`display: none` 元素),也不包含元素的几何位置信息。

4. **布局(Layout / Reflow):计算元素几何位置**
- 浏览器根据渲染树,计算每个元素的**位置(x/y 坐标)** 和**尺寸(width/height)**,生成“布局树”(Layout Tree)。
- 特点:布局结果会影响后续步骤,若元素样式修改导致布局变化(如 `width: 200px` → `300px`),需重新执行布局(即“重排”),开销极大。

5. **重绘(Paint)与合成(Composite):生成像素并显示**
- **重绘**:浏览器根据布局树,将元素的样式(如颜色、背景)绘制到“图层”(Layer)上(生成像素信息),不涉及位置变化。
- **合成**:浏览器将多个图层合并为最终屏幕图像,通过 GPU 渲染到屏幕上(避免 CPU 过载,支持动画高效执行)。

### 三、关键渲染路径的优化策略(核心目标:缩短首屏渲染时间)

优化的核心思路是:**减少阻塞步骤、优先加载关键资源、简化构建流程**,具体分为以下4类:

#### 1. 优先加载“关键资源”(首屏必需的资源)

关键资源是指“首屏渲染必须的最小资源集合”(通常包括:首屏 HTML、关键 CSS、关键 JS),需确保这些资源优先加载,非关键资源延迟加载。

- **识别关键资源**:
通过 Lighthouse 工具(Chrome 开发者工具 → Lighthouse)分析,自动标记首屏必需的 HTML/CSS/JS(非关键资源如页脚 CSS、广告 JS 可延后)。

- **关键 CSS 内联到 HTML**:
关键 CSS(首屏导航、banner、主内容的样式)直接内联到 `<head>` 的 `<style>` 标签中,避免额外 HTTP 请求,让浏览器解析 HTML 时即可获取样式,快速生成渲染树:

```html
<head>
<!-- 内联关键 CSS(仅首屏必需,体积控制在 14KB 以内,避免 HTML 过大) -->
<style>
.header { height: 60px; background: #fff; }
.banner { width: 100%; height: 300px; }
.main-content { padding: 20px; }
</style>
<!-- 异步加载非关键 CSS(如页脚、弹窗样式) -->
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
</head>
```

- **关键 JS 内联或优先加载**:
首屏交互必需的 JS(如按钮点击逻辑)内联到 `<script>` 标签;非关键 JS(如统计、分享功能)用 `defer`/`async` 延迟加载(见下文“减少 JS 阻塞”)。

#### 2. 减少 CSS 渲染阻塞(避免 CSS 阻塞渲染树生成)

CSS 是“渲染阻塞资源”(无 CSSOM 则无法生成渲染树),优化重点是“仅让关键 CSS 阻塞,非关键 CSS 异步加载”。

- **异步加载非关键 CSS**:
用 `<link rel="preload">` 或动态创建 `<link>` 标签,让非关键 CSS 加载时不阻塞 HTML 解析和渲染:

```html
<!-- 方式1:preload + onload 异步加载 -->
<link
rel="preload"
href="non-critical.css"
as="style"
onload="this.onload=null; this.rel='stylesheet'" <!-- 加载完成后应用样式 -->
>
<!-- 降级:不支持 preload 的浏览器 -->
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>

<!-- 方式2:动态创建 link 标签(JS 加载) -->
<script>
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'non-critical.css';
document.head.appendChild(link);
</script>
```

- **精简 CSS 体积**:
- 压缩 CSS(用 `css-minimizer-webpack-plugin` 等工具,去除空格、注释);
- 剔除未使用的 CSS(用 `purgecss` 工具,删除页面中未应用的样式);
- 避免复杂选择器(如 `.a .b .c`),减少 CSSOM 构建时间。

#### 3. 减少 JS 解析阻塞(避免 JS 阻塞 DOM/CSSOM 构建)

JS 是“解析阻塞资源”(同步 JS 会暂停 HTML 解析),且会等待 CSSOM 构建完成后再执行(因 JS 可能读取样式),优化重点是“让 JS 不阻塞首屏关键流程”。

- **区分同步/异步 JS,合理使用 `defer`/`async`**:

| 属性 | 加载行为 | 执行时机 | 适用场景 |
|--------|---------------------------|---------------------------|---------------------------|
| 无 | 同步加载,阻塞 HTML 解析 | 加载完成后立即执行 | 首屏关键 JS(如内联交互) |
| `async`| 异步加载,不阻塞 HTML 解析| 加载完成后立即执行(无序)| 独立脚本(如统计、广告) |
| `defer`| 异步加载,不阻塞 HTML 解析| DOM 解析完成后执行(有序)| 依赖 DOM 的脚本(如初始化)|
示例:

```html
<!-- async:加载完成即执行,不保证顺序 -->
<script src="analytics.js" async></script>
<!-- defer:DOM 解析完执行,按顺序执行 -->
<script src="init-dom.js" defer></script>
<script src="bind-event.js" defer></script> <!-- 比 init-dom 后执行 -->
```

- **延迟加载非关键 JS**:
非首屏 JS(如路由组件、弹窗功能)通过“动态导入”在需要时加载,避免初始阻塞:

```javascript
// 用户点击按钮时,才加载弹窗 JS
document.getElementById('open-modal').addEventListener('click', async () => {
const { Modal } = await import('./modal.js'); // 动态导入
new Modal().show();
});
```

- **避免同步 JS 放在 `<head>` 中**:
同步 JS 若放在 `<head>`,会阻塞 HTML 解析(直到 JS 执行完成),建议:
- 首屏关键 JS 内联到 `<head>`(体积控制在 14KB 以内,避免阻塞过久);
- 非关键同步 JS 放在 `<body>` 末尾(DOM 解析完成后执行)。

#### 4. 简化 DOM/CSSOM 构建与布局步骤

- **减少 DOM 深度和节点数量**:
复杂的 DOM 结构(如嵌套 10+ 层)会增加解析和布局时间,建议:
- 用语义化标签(如 `<header>`、`<main>`)简化结构;
- 避免冗余节点(如空 `<div>`、重复嵌套)。

- **避免强制同步布局**:
若“读取布局属性(如 `offsetHeight`)后立即修改样式”,会强制浏览器重新计算布局,导致阻塞(见“JS 性能优化”中的相关内容),优化方式:

```javascript
// 优化前:强制同步布局(读→改→读→改)
for (let i = 0; i < 100; i++) {
const height = box.offsetHeight; // 读
box.style.height = `${height + 10}px`; // 改(触发布局)
}

// 优化后:先读再改(批量操作)
const height = box.offsetHeight; // 先批量读
for (let i = 0; i < 100; i++) {
box.style.height = `${height + 10 * i}px`; // 再批量改(仅触发1次布局)
}
```

- **用 CSS 合成层优化动画**:
动画优先使用 `transform` 和 `opacity`(仅触发“合成”步骤,不触发布局/重绘),避免使用 `width`、`top` 等触发重排的属性:

```css
/* 高效动画:仅触发合成 */
.box { transition: transform 0.3s; }
.box:hover { transform: translateX(10px); }
```

### 四、总结

关键渲染路径优化的核心是“**聚焦首屏,减少阻塞**”:

1. 优先加载关键资源(内联关键 CSS/JS,异步加载非关键资源);
2. 打破阻塞关系(CSS 异步加载,JS 用 `defer`/`async` 或动态导入);
3. 简化构建步骤(精简 DOM/CSS 体积,避免强制同步布局)。

通过这些策略,可显著缩短首屏渲染时间(FCP),让用户更快看到页面内容,提升核心体验指标。

## 14.什么是缓存?前端缓存的类型有哪些(如 HTTP 缓存、Service Worker 缓存、localStorage)?

### 一、缓存的定义

缓存(Cache)是指**将已获取的资源(如文件、数据)临时存储在本地(浏览器或设备)** 的技术,当后续需要使用该资源时,直接从本地存储读取,无需重新向服务器请求。

缓存的核心价值在于:

1. **提升加载速度**:本地读取资源比网络请求快 10~100 倍,显著减少页面加载时间;
2. **降低网络带宽消耗**:减少重复请求,节省用户流量(尤其弱网环境);
3. **减轻服务器压力**:减少服务器的请求处理量,提升服务稳定性。

前端缓存特指“在浏览器端实现的缓存机制”,主要用于优化网页资源(JS/CSS/图片)和数据(用户配置、临时信息)的复用。

### 二、前端缓存的核心类型(按用途与机制分类)

前端缓存可分为 **“资源缓存”**(针对 JS/CSS/图片等静态资源)和 **“数据缓存”**(针对业务数据),具体包括以下 5 类,核心差异体现在“存储内容、生命周期、灵活性”上:

#### 1. HTTP 缓存(最核心的资源缓存,优先级最高)

HTTP 缓存是浏览器通过 **HTTP 协议头** 控制的缓存机制,针对服务器返回的静态资源(如 JS、CSS、图片、HTML),是前端性能优化中“减少资源重复请求”的关键手段。

它分为 **强缓存** 和 **协商缓存** 两个层级,浏览器会先判断强缓存,未命中再触发协商缓存:

| 类型 | 核心原理 | 关键 HTTP 头 | 特点(是否发请求) | 适用场景 |
|------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------|--------------------------|-----------------------------------|
| **强缓存** | 浏览器判断资源“未过期”时,直接使用本地缓存,不向服务器发任何请求 | - 响应头:`Cache-Control: max-age=3600`(优先级高,单位秒)<br>- 响应头:`Expires: Wed, 21 Oct 2024 07:28:00 GMT`(旧标准,基于服务器时间) | 不发请求,加载最快 | 不常变化的静态资源(如图片、第三方库 JS/CSS) |
| **协商缓存** | 资源过期后,浏览器发送请求到服务器,验证资源是否更新:<br>- 未更新:服务器返回 `304 Not Modified`,浏览器用本地缓存;<br>- 已更新:服务器返回 `200 OK` 和新资源 | - 响应头:`ETag: "5f8d8b2f"` + 请求头:`If-None-Match: "5f8d8b2f"`(基于资源哈希,精度高)<br>- 响应头:`Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT` + 请求头:`If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT`(基于修改时间,精度低) | 发请求,但不返回资源体(304 时) | 可能变化的资源(如业务 JS/CSS、HTML) |

**示例**:服务器返回的 HTTP 响应头(配置强缓存 + 协商缓存):

```http
HTTP/1.1 200 OK
Cache-Control: max-age=3600 # 强缓存:1小时内不发请求
ETag: "5f8d8b2f" # 协商缓存:资源哈希值
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT # 协商缓存:修改时间
Content-Type: text/css
```

**注意事项**:

- `Cache-Control` 优先级高于 `Expires`(避免服务器时间与本地时间不一致导致的误差);
- 若资源需“立即更新”(如紧急修复 bug),可通过“修改资源文件名”(如 `app.8f3d.js` → `app.9a2b.js`)打破强缓存(文件名变化会让浏览器认为是新资源)。

#### 2. Service Worker 缓存(可编程的离线缓存,PWA 核心)

Service Worker 是运行在浏览器主线程外的“独立线程”,可拦截浏览器的网络请求,实现 **自定义缓存策略**(如“离线优先”“网络优先”),是 Progressive Web App(PWA)实现“离线访问”的核心技术。

##### 核心原理

1. **注册 Service Worker**:页面加载时,主线程注册 SW 脚本(需 HTTPS 环境,localhost 除外);
2. **安装(Install)**:SW 首次注册时触发 `install` 事件,此时可缓存关键资源(如首屏 JS/CSS、HTML);
3. **激活(Activate)**:SW 安装完成后触发 `activate` 事件,可清理旧版本缓存;
4. **拦截请求**:SW 激活后,拦截所有网络请求,根据自定义策略决定“返回缓存资源”还是“请求网络”。

##### 代码示例(简化版)

```javascript
// 1. 主线程:注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功', reg))
.catch(err => console.log('SW 注册失败', err));
});
}

// 2. SW 脚本(sw.js):实现缓存逻辑
const CACHE_NAME = 'my-app-cache-v1'; // 缓存版本,更新时修改版本号
const CACHE_RESOURCES = ['/', '/index.html', '/app.js', '/style.css']; // 需缓存的关键资源

// 安装:缓存关键资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(CACHE_RESOURCES)) // 缓存资源
.then(() => self.skipWaiting()) // 强制激活新 SW(跳过等待旧 SW 关闭)
);
});

// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name)) // 删除旧缓存
);
}).then(() => self.clients.claim()) // 控制所有打开的页面
);
});

// 拦截请求:实现“缓存优先”策略(先读缓存,无缓存再请求网络)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request) // 检查缓存中是否有该资源
.then(cachedResponse => {
if (cachedResponse) return cachedResponse; // 有缓存,返回缓存
return fetch(event.request); // 无缓存,请求网络
})
);
});
```

##### 特点与适用场景

- **特点**:独立于主线程(不阻塞渲染)、支持离线访问、可编程(自定义缓存策略)、需 HTTPS;
- **场景**:PWA 应用(如离线文档、新闻 App)、对离线体验要求高的页面(如弱网环境下的工具类页面)。

#### 3. localStorage(持久化数据缓存,容量有限)

localStorage 是浏览器提供的 **持久化键值对存储** 机制,用于存储少量非敏感业务数据(如用户配置、主题偏好),数据存储在浏览器本地,不随请求发送到服务器。

##### 核心特点

- **生命周期**:永久存储(除非手动删除或清除浏览器数据);
- **容量**:约 5~10MB(不同浏览器略有差异);
- **访问权限**:同源页面共享(协议、域名、端口一致);
- **数据类型**:仅支持字符串(存储其他类型需用 `JSON.stringify()` 序列化);
- **操作方式**:同步操作(主线程执行,大量数据会阻塞渲染)。

##### 代码示例

```javascript
// 存储数据(需序列化对象)
const userConfig = { theme: 'dark', fontSize: 16 };
localStorage.setItem('userConfig', JSON.stringify(userConfig));

// 读取数据(需反序列化)
const savedConfig = JSON.parse(localStorage.getItem('userConfig'));
console.log(savedConfig.theme); // "dark"

// 删除数据
localStorage.removeItem('userConfig');

// 清空所有数据
localStorage.clear();
```

##### 适用场景与注意事项

- **场景**:存储用户偏好(如主题、语言)、非敏感的持久化数据(如登录状态 token,注意:token 存储在 localStorage 有 XSS 风险,建议用 HttpOnly Cookie);
- **注意**:不适合存储大量数据(同步操作阻塞主线程)、不存储敏感数据(易被 XSS 攻击窃取)。

#### 4. sessionStorage(会话级数据缓存,临时存储)

sessionStorage 与 localStorage 机制类似,也是 **键值对存储**,但生命周期和作用范围不同,用于存储“临时会话数据”。

##### 核心特点(与 localStorage 对比)

| 特性 | localStorage | sessionStorage |
|--------------|-----------------------------|-----------------------------|
| 生命周期 | 永久(手动删除前) | 会话级(关闭标签页/浏览器后消失) |
| 页面共享 | 同源所有标签页共享 | 仅当前标签页共享(不同标签页不互通) |
| 适用场景 | 持久化数据(如用户配置) | 临时数据(如表单临时值、页面跳转参数) |

##### 代码示例

```javascript
// 存储临时表单数据
sessionStorage.setItem('formData', JSON.stringify({ username: 'test' }));

// 跳转页面后读取(仅当前标签页有效)
const formData = JSON.parse(sessionStorage.getItem('formData'));
```

##### 适用场景

- 临时存储表单输入(如用户填写一半的表单,刷新页面不丢失);
- 页面间临时传递数据(如从列表页跳转到详情页,传递商品 ID)。

#### 5. IndexedDB(大型结构化数据缓存,支持离线)

IndexedDB 是浏览器提供的 **NoSQL 数据库**,用于存储大量结构化数据(如离线文档、复杂表单数据),容量无明确限制(取决于设备存储空间),支持异步操作和事务,适合需离线存储大量数据的场景。

##### 核心特点

- **数据类型**:支持字符串、数字、对象、数组、二进制数据(如图片、文件);
- **操作方式**:异步操作(不阻塞主线程);
- **事务支持**:确保多步操作的原子性(要么全部成功,要么全部失败);
- **访问权限**:同源页面共享,关闭浏览器后数据不消失。

##### 适用场景

- 离线存储大量数据(如离线邮件客户端、离线文档编辑器);
- 存储复杂结构化数据(如用户历史记录、离线报表数据)。

##### 代码示例(简化版:创建数据库并存储数据)

```javascript
// 打开/创建数据库(名称:myDB,版本:1)
const request = indexedDB.open('myDB', 1);

// 数据库首次创建或版本更新时触发
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象仓库(类似数据库表,存储用户数据)
const userStore = db.createObjectStore('users', { keyPath: 'id' }); // id 为主键
// 创建索引(加速查询)
userStore.createIndex('nameIndex', 'name', { unique: false });
};

// 打开数据库成功
request.onsuccess = (event) => {
const db = event.target.result;
// 开启事务(读写模式)
const transaction = db.transaction('users', 'readwrite');
const userStore = transaction.objectStore('users');

// 存储数据
userStore.add({ id: 1, name: 'Alice', age: 25 });

// 事务完成
transaction.oncomplete = () => {
console.log('数据存储成功');
db.close(); // 关闭数据库
};
};
```

### 三、前端缓存类型对比与选择指南

| 缓存类型 | 存储内容 | 生命周期 | 容量 | 核心优势 | 适用场景 |
|----------------|------------------|------------------|------------|---------------------------|-----------------------------------|
| HTTP 缓存 | 静态资源(JS/CSS/图片) | 由 HTTP 头控制 | 无明确限制 | 无需代码,浏览器自动处理 | 所有静态资源的重复请求优化 |
| Service Worker | 静态资源/数据 | 手动控制(版本号)| 无明确限制 | 支持离线、自定义缓存策略 | PWA 应用、离线访问需求 |
| localStorage | 少量业务数据 | 永久(手动删除) | 5~10MB | 简单易用,持久化 | 用户配置、非敏感持久数据 |
| sessionStorage | 临时业务数据 | 会话级(标签页关闭)| 5MB 左右 | 临时存储,不共享 | 表单临时值、页面间临时参数 |
| IndexedDB | 大量结构化数据 | 永久(手动删除) | 无明确限制 | 支持大量数据、异步、事务 | 离线大量数据存储(如离线文档) |

**选择原则**:

1. 静态资源(JS/CSS/图片)→ 优先用 **HTTP 缓存**,需离线则叠加 **Service Worker 缓存**;
2. 少量持久数据(用户配置)→ **localStorage**;
3. 临时数据(表单、跳转参数)→ **sessionStorage**;
4. 大量离线数据(离线文档)→ **IndexedDB**;
5. 离线访问需求(PWA)→ **Service Worker 缓存**。

## 15.简述 HTTP 缓存的工作原理,强缓存(Cache-Control、Expires)和协商缓存(ETag、Last-Modified)的区别

### 一、HTTP 缓存的工作原理

HTTP 缓存是浏览器通过 HTTP 协议头控制的资源缓存机制,核心逻辑是:**当浏览器请求资源时,先检查本地是否有缓存副本,根据缓存规则判断是否可直接使用本地缓存,无需重新请求服务器;若不可用,则向服务器请求资源并更新缓存**。

整体流程分为 3 步:

1. **首次请求**:浏览器向服务器请求资源,服务器返回资源并附带“缓存规则”(通过 HTTP 响应头指定),浏览器存储资源和缓存规则;
2. **再次请求**:浏览器先检查本地缓存,根据缓存规则判断是否“有效”:
- 若有效(强缓存命中):直接使用本地缓存,不向服务器发送请求;
- 若无效(强缓存未命中):向服务器发送请求,携带资源标识(如 ETag、Last-Modified),服务器验证资源是否更新:
- 未更新:返回 `304 Not Modified`,浏览器继续使用本地缓存;
- 已更新:返回 `200 OK` 和新资源,浏览器更新本地缓存。

### 二、强缓存(无需请求服务器)

强缓存是指浏览器通过 **本地缓存规则** 直接判断资源是否“未过期”,若未过期则直接使用本地缓存,**不向服务器发送任何请求**,是性能最优的缓存方式。

#### 核心控制字段

1. **`Cache-Control`(HTTP/1.1,优先级最高)**
响应头字段,通过“相对时间”定义缓存有效期,格式为 `Cache-Control: [指令]`,常用指令:
- `max-age=3600`:资源在 3600 秒(1 小时)内有效(从响应生成时间开始计算);
- `public`:资源可被浏览器和中间代理服务器(如 CDN)缓存;
- `private`:资源仅能被浏览器缓存(中间代理不缓存);
- `no-cache`:不使用强缓存,需强制走协商缓存;
- `no-store`:完全不缓存资源(每次都请求服务器)。

示例:`Cache-Control: max-age=86400, public` 表示资源 24 小时内有效,可被代理缓存。

2. **`Expires`(HTTP/1.0,兼容性字段)**
响应头字段,通过“绝对时间”定义缓存过期时间(格式为 GMT 时间),例如:
`Expires: Wed, 21 Oct 2024 07:28:00 GMT`
浏览器会对比本地时间与该时间,若本地时间早于 `Expires`,则使用缓存。

**缺点**:依赖本地时间(若用户修改本地时间,可能导致缓存提前失效或过期不更新),因此优先级低于 `Cache-Control`(两者同时存在时,`Cache-Control` 生效)。

### 三、协商缓存(需请求服务器验证)

当强缓存过期(或资源被标记为 `no-cache`)时,浏览器会向服务器发送请求,携带资源的“唯一标识”,服务器验证资源是否更新:

- 若未更新:返回 `304 Not Modified`(无资源体),浏览器继续使用本地缓存;
- 若已更新:返回 `200 OK` 和新资源,浏览器更新缓存。

#### 核心控制字段(两组标识)

1. **`Last-Modified` + `If-Modified-Since`(基于修改时间)**
- `Last-Modified`(响应头):服务器返回的资源“最后修改时间”(如 `Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT`);
- `If-Modified-Since`(请求头):浏览器下次请求时,携带 `Last-Modified` 的值,服务器对比该时间与资源当前修改时间:
- 若资源未修改(时间一致):返回 `304`;
- 若资源已修改(时间更新):返回 `200` 和新资源,同时更新 `Last-Modified`。

**缺点**:精度低(仅到秒级),若资源在 1 秒内多次修改,无法区分;若资源内容未变但修改时间更新(如手动编辑保存),会误判为“已更新”。

2. **`ETag` + `If-None-Match`(基于资源哈希,优先级更高)**
- `ETag`(响应头):服务器生成的资源“唯一标识”(通常是内容哈希,如 `ETag: "5f8d8b2f"`),资源内容变化时,`ETag` 必然变化;
- `If-None-Match`(请求头):浏览器下次请求时,携带 `ETag` 的值,服务器对比该标识与资源当前 `ETag`:
- 若标识一致(资源未变):返回 `304`;
- 若标识不一致(资源已变):返回 `200` 和新资源,同时更新 `ETag`。

**优点**:精度高(内容级别的变化检测),适合动态资源(如 PHP 生成的页面)或频繁修改的资源。

### 四、强缓存与协商缓存的核心区别

| 维度 | 强缓存(`Cache-Control`/`Expires`) | 协商缓存(`ETag`/`Last-Modified`) |
|--------------------|--------------------------------------------------|--------------------------------------------------|
| 是否发送请求到服务器 | 不发送(直接用本地缓存) | 发送请求(但可能不返回资源体) |
| 判断依据 | 基于时间(`max-age` 相对时间或 `Expires` 绝对时间) | 基于资源内容(`ETag` 哈希)或修改时间(`Last-Modified`) |
| 响应状态码 | 无(直接使用缓存,无网络请求) | 命中时返回 `304`,未命中返回 `200` |
| 适用场景 | 不常变化的静态资源(如图片、第三方库 JS/CSS) | 可能变化但不频繁的资源(如业务 JS/CSS、HTML) |
| 性能消耗 | 极低(本地读取) | 中等(需网络请求,但数据传输量小) |

### 总结

HTTP 缓存按“优先级”工作:先检查强缓存(`Cache-Control` 优先于 `Expires`),若命中则直接使用缓存;若未命中,触发协商缓存(`ETag` 优先于 `Last-Modified`),由服务器决定是否复用缓存。

这种层级设计既保证了“不常变资源”的快速加载(强缓存),又解决了“可能变化资源”的更新问题(协商缓存),最终实现“减少请求、提升性能”的核心目标。

## 16.如何配置 HTTP 缓存策略?不同资源(如 HTML、CSS、JS、图片)的缓存策略有何不同?

### 一、HTTP 缓存策略的核心配置逻辑

HTTP 缓存策略通过**服务器响应头**(强缓存 `Cache-Control`/`Expires` + 协商缓存 `ETag`/`Last-Modified`)和**资源版本控制(文件指纹)** 组合实现,核心原则是:

- **内容不变的资源**:配置**长强缓存**(减少请求),配合文件指纹(内容变则文件名变,打破旧缓存);
- **可能变化的资源**:配置**短强缓存+协商缓存**(确保及时更新,同时减少重复资源传输)。

配置的核心是通过服务器(如 Nginx、Apache)或 CDN 统一设置响应头,而非前端代码直接控制。

### 二、通用 HTTP 缓存配置方法(服务器/CDN)

以下以最常用的 **Nginx** 为例,展示如何通过配置文件设置缓存头,其他服务器(Apache、IIS)或 CDN(阿里云、Cloudflare)逻辑类似,仅语法差异。

#### 1. 基础配置:启用协商缓存(ETag + Last-Modified)

Nginx 默认启用 `ETag` 和 `Last-Modified`,无需额外配置;若需手动开启,可添加:

```nginx
http {
# 启用 ETag(默认 on,可省略)
etag on;
# 启用 Last-Modified(默认通过文件修改时间生成,可省略)
server_tokens off; # 避免服务器版本泄露,不影响 Last-Modified
}
```

#### 2. 按资源类型配置强缓存(Cache-Control)

通过 `location` 匹配不同资源后缀,设置不同的 `Cache-Control`(强缓存核心),结合文件指纹实现“内容变则缓存失效”。

完整 Nginx 配置示例:

```nginx
server {
listen 80;
server_name example.com;

# 1. HTML 文件:入口资源,需及时更新 → 无强缓存+协商缓存
location ~* \.html$ {
root /usr/share/nginx/html;
# Cache-Control: no-cache → 不启用强缓存,每次请求走协商缓存
# public → 允许代理(如 CDN)缓存
add_header Cache-Control "public, no-cache";
# 可选:添加 ETag(默认启用,可省略)
etag on;
}

# 2. CSS/JS 文件:有文件指纹(如 app.8f3d.js)→ 长强缓存+协商缓存
location ~* \.(css|js)$ {
root /usr/share/nginx/html;
# max-age=31536000 → 强缓存 1 年
# immutable → 告诉浏览器“资源不会变”,无需验证协商缓存(优化性能)
# public → 允许 CDN 缓存
add_header Cache-Control "public, max-age=31536000, immutable";
# 启用 ETag,备用协商缓存
etag on;
}

# 3. 图片文件(JPG/PNG/WebP 等):稳定少变 → 长强缓存+协商缓存
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
root /usr/share/nginx/html;
add_header Cache-Control "public, max-age=31536000, immutable";
etag on;
# 可选:压缩图片(进一步优化性能)
image_filter resize 1920 0; # 限制最大宽度 1920px
image_filter_jpeg_quality 80; # JPG 质量 80%
}

# 4. 用户上传图片(如 avatar.jpg):可能变化 → 短强缓存+协商缓存
location ~* /uploads/.*\.(jpg|png)$ {
root /usr/share/nginx/html;
# max-age=86400 → 强缓存 1 天,避免频繁请求
add_header Cache-Control "public, max-age=86400";
etag on;
}
}
```

#### 3. CDN 缓存配置(补充)

若资源通过 CDN 分发,需在 CDN 控制台同步配置缓存策略,核心是:

- **缓存键(Cache Key)**:默认按 URL 缓存,确保文件指纹(如 `app.8f3d.js`)包含在 URL 中;
- **TTL(缓存有效期)**:与服务器 `max-age` 保持一致(如 CSS/JS 设为 1 年,HTML 设为 0);
- **源站同步**:CDN 缓存过期后,自动向源站请求最新资源(触发协商缓存)。

### 三、不同资源的缓存策略差异(核心原因+配置)

不同资源的“更新频率”和“功能角色”不同,决定了缓存策略的差异,具体如下:

#### 1. HTML 文件(入口资源)

- **核心特点**:
- 页面入口,控制 CSS/JS 等资源的加载路径(如 `<script src="app.8f3d.js">`);
- 需及时更新(若 HTML 缓存旧版本,会导致加载旧的 CSS/JS,页面异常)。
- **缓存策略**:**无强缓存 + 强制协商缓存**
- `Cache-Control: public, no-cache`:不启用强缓存,每次请求都向服务器验证(协商缓存);
- 禁用 `max-age` 或设为 `max-age=0`,确保用户每次获取最新 HTML;
- 必须启用 `ETag`/`Last-Modified`,避免 HTML 重复传输。
- **为什么不设长强缓存**:若 HTML 强缓存 1 年,用户修改内容后,浏览器仍用旧 HTML,无法加载新资源。

#### 2. CSS/JS 文件(业务逻辑/样式)

- **核心特点**:
- 内容变化时,可通过**文件指纹**(如 `app.[contenthash].js`)修改文件名;
- 无入口依赖(依赖由 HTML 控制),适合长缓存。
- **缓存策略**:**长强缓存 + 协商缓存 + immutable**
- `Cache-Control: public, max-age=31536000, immutable`:
- `max-age=31536000`:强缓存 1 年,减少请求;
- `immutable`:告诉浏览器“资源不会变”,无需发起协商缓存请求(优化性能,现代浏览器支持);
- 启用 `ETag`:备用(若文件指纹失效,协商缓存可兜底);
- **关键前提**:构建工具(webpack/Vite)需生成文件指纹,内容变则指纹变,浏览器认为是新资源,自动打破旧缓存。

#### 3. 图片文件(静态资源)

- **核心特点**:
- 内容稳定(如 logo、图标、banner),极少修改;
- 体积较大,长缓存可显著减少带宽消耗。
- **缓存策略**:**长强缓存 + 协商缓存 + immutable**
- 配置与 CSS/JS 一致:`Cache-Control: public, max-age=31536000, immutable`;
- 配合文件指纹(如 `logo.[hash].png`),修改图片时更新文件名;
- **特殊情况(用户上传图)**:
- 如头像、商品图,可能频繁更新,设短强缓存:`max-age=86400`(1 天),配合 `ETag` 确保更新。

#### 4. 特殊资源(动态/临时资源)

- **动态资源**(如 PHP/JSP 生成的页面):
- 特点:内容实时变化(如用户个人中心);
- 策略:`Cache-Control: no-cache, no-store`:不缓存,每次请求都获取最新内容;
- **临时资源**(如验证码图片):
- 特点:一次性使用,不可复用;
- 策略:`Cache-Control: no-cache, no-store, must-revalidate`:禁止缓存,强制每次请求。

### 四、配置原则与避坑指南

#### 1. 核心原则

- **“内容不变则缓存不变,内容变则缓存失效”**:通过文件指纹(contenthash)实现,而非依赖时间;
- **“入口资源(HTML)弱缓存,依赖资源(CSS/JS/图片)强缓存”**:避免入口缓存导致依赖资源无法更新;
- **“CDN 与源站策略一致”**:CDN 的 TTL 需与源站 `max-age` 匹配,避免缓存不一致。

#### 2. 常见坑点

- **HTML 设长强缓存**:导致用户无法获取更新,需手动清除缓存;
- **忘记加文件指纹**:CSS/JS 内容变了,但文件名不变,浏览器仍用旧缓存;
- **禁用 ETag**:协商缓存失效,即使资源未变,也返回 200 并传输完整资源;
- **跨域资源未设 `public`**:`Cache-Control: private` 会导致 CDN 无法缓存,仅浏览器缓存。

### 五、不同资源缓存策略对比表

| 资源类型 | 核心需求 | Cache-Control 配置 | 协商缓存 | 文件指纹 | 适用场景 |
|----------|-------------------------|-------------------------------------|----------|----------|---------------------------|
| HTML | 及时更新,控制入口 | public, no-cache | 必须启用 | 无需 | 所有页面入口文件 |
| CSS/JS | 长缓存,内容变则更新 | public, max-age=31536000, immutable | 建议启用 | 必须 | 业务样式、逻辑代码 |
| 静态图片 | 长缓存,减少带宽 | public, max-age=31536000, immutable | 建议启用 | 必须 | logo、图标、固定 banner |
| 用户上传图 | 可能更新,避免旧图 | public, max-age=86400 | 必须启用 | 可选 | 头像、商品图、动态图片 |
| 动态资源 | 实时更新,不可缓存 | no-cache, no-store | 禁用 | 无需 | 验证码、个人中心动态内容 |

### 总结

HTTP 缓存配置的核心是“按资源特性差异化策略”:

- 入口 HTML 优先“更新”,用协商缓存;
- 依赖资源(CSS/JS/图片)优先“性能”,用长强缓存+文件指纹;
- 配置时需结合服务器/CDN,确保策略一致,同时通过文件指纹解决“缓存更新”问题,最终实现“加载快、更新及时”的目标。

## 17.什么是 Service Worker?它的作用是什么?如何使用 Service Worker 实现离线缓存(PWA)?

### 一、Service Worker 是什么?

Service Worker 是**运行在浏览器后台的独立脚本**(不属于网页主线程),本质是一种“代理服务器”,可以拦截网页的网络请求、管理缓存资源、实现离线功能等。

它的核心特点:

- **独立线程**:运行在后台,不阻塞网页渲染和交互(与主线程通过 `postMessage` 通信);
- **生命周期**:与网页无关(即使关闭网页,Service Worker 仍可运行),需手动安装、激活、更新;
- **安全限制**:仅在 HTTPS 环境或 `localhost` 中运行(防止中间人攻击);
- **事件驱动**:通过监听 `install`、`activate`、`fetch` 等事件实现功能。

### 二、Service Worker 的核心作用

Service Worker 是 Progressive Web App(PWA,渐进式网页应用)的核心技术,主要解决以下问题:

1. **实现离线访问**
缓存关键资源(HTML、CSS、JS、图片),当用户离线或网络不稳定时,直接从本地缓存返回资源,避免“无法访问”的空白页。

2. **拦截与代理网络请求**
可以自定义请求处理逻辑(如“缓存优先”“网络优先”“缓存回退”),优化资源加载策略(如弱网环境优先用缓存)。

3. **后台同步**
当用户离线时,将数据操作(如表单提交)暂存到本地,待网络恢复后自动同步到服务器(通过 `Background Sync API`)。

4. **推送通知**
配合 `Push API` 实现类似原生 App 的推送通知(即使网页关闭,也能收到通知),提升用户召回率。

### 三、使用 Service Worker 实现离线缓存(PWA 核心步骤)

以下是通过 Service Worker 实现“关键资源离线缓存”的完整流程,包含代码示例和核心逻辑:

#### 1. 注册 Service Worker(主线程)

在网页加载时,通过主线程(如 `index.js`)注册 Service Worker 脚本,让浏览器识别并启动它。

```javascript
// 主线程代码(如 index.js)
if ('serviceWorker' in navigator) {
// 页面加载完成后注册,避免阻塞首屏
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js') // 注册 sw.js 脚本
.then(registration => {
console.log('Service Worker 注册成功,作用域:', registration.scope);
})
.catch(error => {
console.log('Service Worker 注册失败:', error);
});
});
}
```

- **注意**:`sw.js` 必须放在网站根目录(`/sw.js`),否则默认作用域仅限脚本所在目录(如 `/js/sw.js` 只能控制 `/js/` 下的页面)。

#### 2. 安装(Install):缓存关键资源

Service Worker 首次注册或更新时,触发 `install` 事件,此时可缓存首屏必需的资源(如 HTML、CSS、JS、核心图片)。

```javascript
// Service Worker 脚本(sw.js)
const CACHE_NAME = 'my-pwa-cache-v1'; // 缓存版本号(更新时需修改,如 v2)
// 需缓存的关键资源列表(根据实际需求调整)
const CACHE_RESOURCES = [
'/',
'/index.html',
'/css/style.css',
'/js/app.js',
'/images/logo.png',
'/offline.html' // 离线时的备用页面
];

// 监听 install 事件:缓存关键资源
self.addEventListener('install', (event) => {
// 等待缓存完成后再结束 install 阶段
event.waitUntil(
// 打开指定名称的缓存空间
caches.open(CACHE_NAME)
.then(cache => {
console.log('开始缓存资源');
// 批量缓存资源(返回 Promise)
return cache.addAll(CACHE_RESOURCES);
})
.then(() => {
// 强制激活新 Service Worker(跳过等待旧 SW 控制的页面关闭)
return self.skipWaiting();
})
);
});
```

- **关键**:`CACHE_NAME` 需包含版本号(如 `v1`),后续更新资源时,修改版本号(如 `v2`)即可触发新 SW 的安装。

#### 3. 激活(Activate):清理旧缓存

新 Service Worker 安装完成后,触发 `activate` 事件,此时需清理旧版本的缓存(避免缓存冗余),并接管所有打开的页面。

```javascript
// 监听 activate 事件:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
// 获取所有缓存名称
caches.keys().then(cacheNames => {
return Promise.all(
// 过滤出旧版本缓存并删除
cacheNames.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
}).then(() => {
// 让新 SW 立即控制所有打开的页面(无需刷新)
return self.clients.claim();
})
);
});
```

#### 4. 拦截请求(Fetch):实现离线缓存策略

Service Worker 激活后,会拦截网页的所有网络请求(`fetch` 事件),此时可自定义“从缓存读取”还是“请求网络”的逻辑(核心缓存策略)。

常用策略:**“缓存优先,网络回退”**(优先用缓存,无缓存则请求网络,适合静态资源)。

```javascript
// 监听 fetch 事件:拦截请求并处理
self.addEventListener('fetch', (event) => {
// 对 HTML 请求使用“网络优先,缓存回退”(确保内容最新)
if (event.request.headers.get('Accept').includes('text/html')) {
event.respondWith(
fetch(event.request)
.then(networkResponse => {
// 网络请求成功,更新缓存(保证缓存是最新的)
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
})
.catch(() => {
// 网络失败,返回缓存的 HTML 或离线页面
return caches.match(event.request).then(cacheResponse => {
// 若缓存中没有该页面,返回通用离线页
return cacheResponse || caches.match('/offline.html');
});
})
);
return; // 处理完 HTML 请求,跳出函数
}

// 对其他资源(CSS/JS/图片)使用“缓存优先,网络回退”
event.respondWith(
caches.match(event.request) // 先查缓存
.then(cacheResponse => {
// 若缓存命中,直接返回缓存
if (cacheResponse) return cacheResponse;

// 缓存未命中,请求网络
return fetch(event.request)
.then(networkResponse => {
// 网络请求成功,更新缓存(供下次使用)
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
})
.catch(() => {
// 网络失败且无缓存(如图片),返回默认占位图
if (event.request.destination === 'image') {
return caches.match('/images/placeholder.png');
}
});
})
);
});
```

#### 5. 验证离线功能

1. 部署代码到 HTTPS 环境(或 `localhost`),打开页面,确保 Service Worker 注册成功(Chrome 开发者工具 → `Application` → `Service Workers`);
2. 在开发者工具中勾选 `Offline`(模拟离线),刷新页面,若能正常显示内容(从缓存加载),则离线功能生效。

### 四、Service Worker 离线缓存的注意事项

1. **缓存版本管理**:更新资源时,必须修改 `CACHE_NAME`(如 `v1` → `v2`),否则浏览器会认为是旧 SW,不触发更新;
2. **缓存资源体积**:避免缓存过多资源(尤其是大图片),否则会占用用户存储空间,影响性能;
3. **调试技巧**:Chrome 开发者工具 `Application` 面板可查看缓存内容、强制更新 SW、模拟离线环境;
4. **兼容性**:支持 Chrome 40+、Firefox 44+、Edge 17+、Safari 11.1+,旧浏览器需优雅降级(不影响正常使用)。

### 总结

Service Worker 是实现 PWA 离线功能的核心技术,通过“注册-安装-激活-拦截请求”的生命周期,实现资源缓存和离线访问。其核心价值在于:**让网页在弱网/离线环境下仍能正常使用,接近原生 App 的体验**。结合 `Cache API` 和灵活的缓存策略,可显著提升页面可靠性和用户体验。

## 18.什么是 PWA(渐进式 Web 应用)?PWA 的核心特性有哪些(如离线访问、推送通知、添加到桌面)?

### 一、PWA(渐进式 Web 应用)的定义

PWA(Progressive Web App)是**结合了 Web 技术和原生 App 体验的新型应用形态**,通过现代 Web 技术(如 Service Worker、Web App Manifest 等)实现“渐进式增强”——既保留 Web 应用“无需下载安装、跨平台访问”的便捷性,又具备原生 App 的核心体验(如离线可用、推送通知、桌面图标等)。

其核心理念是“**渐进式**”:无需一次性实现所有功能,可根据需求逐步添加特性,且能在各种设备和浏览器上正常工作(旧浏览器自动降级为普通 Web 页面)。

### 二、PWA 的核心特性(附技术实现与用户价值)

PWA 的核心特性围绕“**提升用户体验**”和“**增强可用性**”设计,主要包括以下 6 点:

#### 1. 离线访问(Offline Capability)

- **功能描述**:网络不稳定或离线时,仍能加载并使用核心功能(如阅读已缓存的文章、查看本地数据),避免“无法访问”的空白页。
- **技术实现**:通过 **Service Worker**(后台代理脚本)拦截网络请求,结合 **Cache API** 缓存关键资源(HTML、CSS、JS、数据),自定义“缓存优先”或“网络优先”策略。
- **用户价值**:解决弱网/离线场景的使用痛点(如地铁、偏远地区),提升应用可靠性。

#### 2. 推送通知(Push Notifications)

- **功能描述**:类似原生 App,即使关闭网页,也能向用户发送推送通知(如消息提醒、活动通知),召回用户回访。
- **技术实现**:
- **Push API**:服务器向浏览器推送消息;
- **Service Worker**:在后台接收并显示通知(独立于网页主线程)。
- **用户价值**:提升用户粘性(如电商的促销提醒、社交 App 的消息通知),无需打开 App 即可触达用户。

#### 3. 添加到桌面(Add to Home Screen)

- **功能描述**:用户可将 PWA 添加到手机/电脑桌面,生成独立图标,点击直接打开(无需通过浏览器地址栏访问),接近原生 App 的启动体验。
- **技术实现**:通过 **Web App Manifest**(JSON 文件)配置应用名称、图标、启动页、显示模式(如全屏、无地址栏)等,浏览器检测到符合条件后会提示用户“添加到桌面”。
示例 `manifest.json`:

```json
{
"name": "我的 PWA 应用",
"short_name": "PWA", // 桌面图标显示的短名称
"start_url": "/", // 启动时打开的页面
"display": "standalone", // 独立窗口模式(无浏览器工具栏)
"background_color": "#fff", // 启动屏背景色
"theme_color": "#4285f4", // 标题栏颜色
"icons": [
{ "src": "icon-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icon-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}
```

- **用户价值**:降低用户访问门槛(无需记住网址),提升使用频率。

#### 4. 响应式设计(Responsive Design)

- **功能描述**:在任何设备(手机、平板、电脑)上都能自适应显示,布局和交互根据屏幕尺寸自动调整(如移动端单列、桌面端多列)。
- **技术实现**:通过 CSS 媒体查询(`@media`)、弹性布局(Flexbox)、网格布局(Grid)等实现。
- **用户价值**:统一跨设备体验,避免“移动端适配差”的问题。

#### 5. 安全可靠(Secure & Reliable)

- **功能描述**:通过 HTTPS 传输数据,防止内容被篡改或窃取;加载速度稳定(依赖缓存),避免因网络波动导致的加载失败。
- **技术实现**:
- 强制使用 **HTTPS**(Service Worker、Push API 等核心功能仅在 HTTPS 环境生效);
- Service Worker 缓存确保资源加载稳定性。
- **用户价值**:保护用户数据安全,提供可预期的加载体验。

#### 6. 渐进式增强(Progressive Enhancement)

- **功能描述**:在支持 PWA 特性的浏览器上提供完整体验(如离线、推送),在旧浏览器上自动降级为普通 Web 页面(不影响基础功能使用)。
- **技术实现**:通过特性检测(如 `if ('serviceWorker' in navigator)`)判断浏览器支持度,选择性启用高级功能。
- **用户价值**:覆盖所有用户(无论设备/浏览器新旧),无需因兼容性牺牲功能。

### 三、PWA 与传统 Web 应用、原生 App 的对比

| 特性 | 传统 Web 应用 | 原生 App | PWA |
|---------------------|-----------------------------|----------------------------|------------------------------|
| 安装方式 | 无需安装(通过浏览器访问) | 需从应用商店下载安装 | 无需安装(可添加到桌面) |
| 离线访问 | 无(依赖网络) | 支持 | 支持(Service Worker 缓存) |
| 推送通知 | 无 | 支持 | 支持(Push API) |
| 跨平台 | 支持(依赖浏览器) | 不支持(需为 iOS/Android 分别开发) | 支持(一套代码多平台运行) |
| 更新方式 | 自动更新(刷新页面) | 需通过应用商店下载更新 | 自动更新(Service Worker 控制)|

### 总结

PWA 的核心是通过 **Service Worker**(离线/缓存)、**Web App Manifest**(桌面图标)、**Push API**(推送通知)等技术,让 Web 应用具备“**离线可用、随时触达、跨平台、低门槛**”的优势,同时保留 Web 应用“无需安装、即时更新”的便捷性。适合新闻阅读、电商、工具类等需要高频访问和离线场景的应用,是提升用户体验的重要方案。

## 19.如何优化 DOM 性能?(如减少 DOM 操作、批量操作 DOM、使用虚拟 DOM、避免强制同步布局)

DOM 性能优化的核心是**减少浏览器重排(Reflow)和重绘(Repaint)的频率与成本**。DOM 是浏览器中性能消耗较高的部分——每一次 DOM 操作(如新增、修改、删除节点)都可能触发页面布局计算(重排)或像素绘制(重绘),而频繁的操作会导致页面卡顿、交互延迟。以下是具体优化策略及实现方式:

### 一、减少 DOM 操作次数:避免频繁修改

DOM 操作的性能瓶颈不在于单次操作本身,而在于“频繁操作触发的多次重排/重绘”。例如,在循环中逐次修改 DOM 会导致每一次修改都触发重排,性能开销极大。

#### 优化思路:**合并操作,批量执行**

1. **先在内存中准备内容,再一次性更新 DOM**
用字符串拼接、文档片段(DocumentFragment)或离线节点暂存所有修改,最后一次性插入 DOM,仅触发**1次重排**。

- 反例(频繁触发重排):

```javascript
const list = document.getElementById('list');
// 循环中每次都修改 DOM,触发多次重排
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // 每次 append 都触发重排
}
```

- 正例(批量操作,1次重排):

```javascript
const list = document.getElementById('list');
// 1. 用文档片段(DocumentFragment)暂存节点(不触发重排)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item); // 内存中操作,无重排
}
// 2. 一次性插入 DOM,仅触发1次重排
list.appendChild(fragment);
```

2. **先隐藏元素,操作完成后再显示**
对元素设置 `display: none` 后,其 DOM 操作不会触发重排(元素脱离文档流),操作完成后恢复显示,仅触发**2次重排**(隐藏和显示各1次)。

```javascript
const container = document.getElementById('container');
// 1. 隐藏元素(触发1次重排)
container.style.display = 'none';
// 2. 批量修改(无重排)
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Content ${i}`;
container.appendChild(div);
}
// 3. 恢复显示(触发1次重排)
container.style.display = 'block';
```

### 二、简化 DOM 结构:减少节点数量与层级

复杂的 DOM 结构(如嵌套层级过深、冗余节点过多)会增加浏览器解析和布局计算的成本,尤其是在重排时(需递归计算所有相关节点的位置)。

#### 优化思路:**精简结构,减少嵌套**

1. **删除冗余节点**:移除空节点(如 `<div></div>`)、重复嵌套的容器(如 `<div><div><p>...</p></div></div>` 可简化为 `<p>...</p>`)。
2. **控制嵌套层级**:建议 DOM 嵌套不超过 6 层(浏览器对深层级 DOM 的渲染效率会显著下降)。
3. **使用语义化标签**:用 `<header>`、`<main>`、`<footer>` 等替代多层 `<div>`,既简化结构又提升可读性。

### 三、使用虚拟 DOM(Virtual DOM):减少真实 DOM 操作

虚拟 DOM 是**内存中的 JavaScript 对象**,用于描述真实 DOM 的结构。当数据变化时,先在虚拟 DOM 中计算“新旧结构的差异”,再将差异批量应用到真实 DOM,从而减少真实 DOM 的操作次数(避免不必要的重排/重绘)。

#### 核心原理

1. 用 JS 对象模拟 DOM 树(如 `{ tag: 'div', props: { class: 'box' }, children: [...] }`);
2. 数据更新时,生成新的虚拟 DOM,与旧虚拟 DOM 对比(diff 算法),得到最小修改集;
3. 仅将差异部分转换为真实 DOM 操作,批量执行。

#### 优势

- 减少真实 DOM 操作次数(虚拟 DOM 操作在内存中完成,成本极低);
- 抽象 DOM 操作,简化复杂场景(如列表更新、条件渲染)的性能优化。

#### 应用示例(React 中的虚拟 DOM)

React 会自动将 JSX 转换为虚拟 DOM,通过 `ReactDOM.render` 将差异应用到真实 DOM:

```jsx
// JSX 被转换为虚拟 DOM 对象
function App() {
const [count, setCount] = React.useState(0);
return (
<div className="app">
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// 仅更新变化的部分(count 数值),而非整个组件
ReactDOM.render(<App />, document.getElementById('root'));
```

### 四、避免强制同步布局(Forced Synchronous Layouts)

浏览器会优化重排过程,将多次布局修改合并为一次执行(异步布局)。但如果**先读取布局属性(如 `offsetHeight`),再立即修改样式**,会强制浏览器“同步刷新布局”(立即计算最新布局),导致额外性能开销。

#### 问题示例(强制同步布局)

```javascript
const boxes = document.querySelectorAll('.box');
// 错误:读 → 改 → 读 → 改,每次都强制刷新布局
boxes.forEach(box => {
const height = box.offsetHeight; // 读取布局(触发同步计算)
box.style.height = `${height + 10}px`; // 立即修改,再次触发布局
});
```

#### 优化方案(先读后改,批量操作)

```javascript
const boxes = document.querySelectorAll('.box');
// 1. 先批量读取所有布局属性(仅触发1次布局计算)
const heights = Array.from(boxes).map(box => box.offsetHeight);

// 2. 再批量修改样式(仅触发1次布局计算)
boxes.forEach((box, index) => {
box.style.height = `${heights[index] + 10}px`;
});
```

### 五、其他关键优化策略

1. **使用 CSS `contain` 隔离渲染范围**
对独立组件(如弹窗、侧边栏)添加 `contain: layout paint size`,告诉浏览器“该元素的渲染不会影响其他部分”,限制重排/重绘的范围:

```css
.modal {
contain: layout paint size; /* 隔离渲染,重排/重绘仅作用于该元素 */
position: fixed;
width: 500px;
height: 300px;
}
```

2. **避免使用昂贵的 DOM API**
- 减少 `offsetTop`、`scrollTop`、`getBoundingClientRect()` 等“触发布局”的 API 调用(如需使用,批量读取);
- 优先用 `getElementById`(O(1) 复杂度)而非 `querySelectorAll`(需遍历 DOM);
- 用 `textContent` 替代 `innerHTML`(避免 HTML 解析,更安全且高效)。

3. **使用事件委托减少事件监听**
对列表等动态生成的元素,将事件监听绑定到父级(而非每个子元素),减少内存占用和 DOM 操作:

```javascript
// 反例:给每个子元素绑定事件(DOM 操作多,内存占用高)
const items = document.querySelectorAll('.list-item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});

// 正例:事件委托(仅绑定1次,支持动态元素)
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
if (e.target.classList.contains('list-item')) {
handleClick(e); // 仅处理子元素的点击
}
});
```

### 总结

DOM 性能优化的核心逻辑是:**减少重排/重绘的次数和范围**。具体可通过:

- 批量操作 DOM(文档片段、先隐藏后修改);
- 简化 DOM 结构(减少节点和层级);
- 用虚拟 DOM 减少真实 DOM 操作;
- 避免强制同步布局和昂贵 API;
- 隔离渲染范围(`contain` 属性)。

这些策略能显著降低浏览器的渲染开销,提升页面流畅度,尤其在处理长列表、动态内容时效果明显。

## 20.什么是虚拟列表?虚拟列表的作用是什么?如何实现虚拟列表(如固定高度、动态高度)?

### 一、虚拟列表(Virtual List)的定义

虚拟列表(又称“虚拟滚动”)是一种**长列表性能优化技术**,核心原理是:**只渲染可视区域内的列表项,而非全部数据**。即使列表数据量极大(如10万条),DOM中也只保留“当前可见+少量缓冲”的节点(通常不超过50个),从而减少DOM操作和渲染压力。

### 二、虚拟列表的核心作用

当列表数据量庞大(如上千/上万条)时,传统做法是一次性渲染所有项,会导致:

- **DOM节点过多**:浏览器解析和渲染成本剧增,页面加载缓慢;
- **频繁重排重绘**:滚动时需计算所有节点的位置,导致卡顿、掉帧;
- **内存占用过高**:大量DOM节点占用内存,可能引发页面崩溃。

虚拟列表通过“按需渲染”解决这些问题,核心价值:

1. **降低DOM节点数量**:无论总数据量多少,DOM节点数控制在固定范围(如30~50个);
2. **提升滚动流畅度**:减少重排重绘成本,滚动时保持60fps(帧/秒);
3. **优化内存占用**:避免大量节点占用内存,适合移动端等资源有限的环境。

### 三、虚拟列表的实现方式(固定高度 vs 动态高度)

虚拟列表的核心逻辑是“**计算可视区域范围→渲染对应项→通过偏移模拟滚动位置**”,固定高度和动态高度的实现差异主要在“高度计算”环节。

#### 1. 固定高度虚拟列表(基础版,适合高度统一的场景)

**前提**:所有列表项高度相同(如固定50px),可通过数学计算直接定位每个项的位置。

##### 核心步骤

- **确定容器与可视区域**:容器设固定高度并开启滚动(`overflow: auto`);
- **计算可视范围**:根据滚动距离,计算当前可见的列表项索引(`startIndex` ~ `endIndex`);
- **渲染可见项**:只渲染 `startIndex` 到 `endIndex` 之间的项;
- **模拟滚动偏移**:通过 `paddingTop` 或 `transform: translateY()` 撑开容器顶部,让可见项显示在正确位置。

##### 代码实现(固定高度)

```html
<!-- HTML:容器结构 -->
<div id="container" class="virtual-list-container">
<!-- 占位容器:用于撑开高度,模拟滚动条长度 -->
<div id="placeholder" class="placeholder"></div>
<!-- 可见项容器:实际渲染的列表项 -->
<div id="visibleItems" class="visible-items"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* CSS:样式设置 */
.virtual-list-container {
height: 400px; /* 可视区域高度 */
overflow: auto;
position: relative;
border: 1px solid #ccc;
}

.placeholder {
/* 高度会通过JS设置为总数据高度(总条数×单条高度),用于显示正确的滚动条 */
}

.visible-items {
position: absolute;
top: 0;
left: 0;
width: 100%;
}

.list-item {
height: 50px; /* 固定项高度 */
padding: 10px;
border-bottom: 1px solid #eee;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// JavaScript:核心逻辑
class VirtualList {
constructor({
container, // 容器DOM
data, // 总数据
itemHeight = 50, // 每项固定高度
buffer = 5 // 可视区域外的缓冲项数量(避免快速滚动时空白)
}) {
this.container = container;
this.data = data;
this.itemHeight = itemHeight;
this.buffer = buffer;
this.visibleItems = container.querySelector('#visibleItems');
this.placeholder = container.querySelector('#placeholder');

// 初始化
this.init();
}

init() {
// 1. 设置占位容器高度(总高度 = 数据量 × 单条高度)
this.placeholder.style.height = `${this.data.length * this.itemHeight}px`;

// 2. 监听滚动事件(节流处理,避免频繁触发)
this.container.addEventListener('scroll', this.handleScroll.bind(this));

// 3. 初始渲染
this.renderVisibleItems();
}

// 计算可视区域内的项索引
getVisibleRange() {
const { scrollTop, clientHeight } = this.container;
// 可视区域开始索引(向上取整)
const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
// 可视区域结束索引(向下取整 + 缓冲)
const endIndex = Math.min(
this.data.length - 1,
Math.floor((scrollTop + clientHeight) / this.itemHeight) + this.buffer
);
return { startIndex, endIndex };
}

// 渲染可视区域的项
renderVisibleItems() {
const { startIndex, endIndex } = this.getVisibleRange();
const visibleData = this.data.slice(startIndex, endIndex + 1);

// 计算偏移量(让可见项显示在正确位置)
const offsetTop = startIndex * this.itemHeight;
this.visibleItems.style.transform = `translateY(${offsetTop}px)`;

// 渲染可见项
this.visibleItems.innerHTML = visibleData
.map((item, index) => `
<div class="list-item" data-index="${startIndex + index}">
${item.content}
</div>
`)
.join('');
}

// 滚动事件处理
handleScroll() {
// 节流:使用requestAnimationFrame确保在重绘前执行
requestAnimationFrame(() => {
this.renderVisibleItems();
});
}
}

// 用法示例
const data = Array.from({ length: 10000 }, (_, i) => ({
content: `列表项 ${i + 1}`
}));

new VirtualList({
container: document.getElementById('container'),
data,
itemHeight: 50
});

2. 动态高度虚拟列表(进阶版,适合高度不固定的场景)

前提:列表项高度不统一(如文本长度不同导致高度变化),无法通过固定值计算,需“预估高度+实际测量”结合。

核心挑战
  • 无法预先知道每个项的真实高度,难以计算滚动范围和偏移量;
  • 滚动时可能因实际高度与预估高度偏差,导致内容错位或滚动条跳动。
解决方案
  1. 预估高度:初始为每个项设置一个预估高度(如平均高度),用于计算大致的滚动范围和偏移量;
  2. 缓存真实高度:当项首次渲染后,立即测量其真实高度并缓存;
  3. 动态调整偏移:滚动时,若遇到已缓存真实高度的项,用真实值修正偏移量,避免错位;
  4. 重新计算总高度:随着真实高度被缓存,逐步修正占位容器的总高度,使滚动条长度更准确。
关键代码差异(动态高度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class DynamicVirtualList {
constructor(options) {
// 继承固定高度的基础属性,增加:
this.estimatedHeight = 80; // 预估高度(可根据内容类型调整)
this.heightMap = new Map(); // 缓存已渲染项的真实高度(key: index, value: height)
}

// 重写:计算某索引位置的累计高度(结合缓存的真实高度)
getOffsetTop(index) {
let total = 0;
for (let i = 0; i < index; i++) {
// 优先用真实高度,否则用预估高度
total += this.heightMap.get(i) || this.estimatedHeight;
}
return total;
}

// 重写:计算可视范围(基于累计高度)
getVisibleRange() {
const { scrollTop, clientHeight } = this.container;
let startIndex = 0;
let endIndex = this.data.length - 1;
let currentOffset = 0;

// 找到滚动位置对应的起始索引(遍历计算累计高度)
for (let i = 0; i < this.data.length; i++) {
const height = this.heightMap.get(i) || this.estimatedHeight;
if (currentOffset + height > scrollTop) {
startIndex = i;
break;
}
currentOffset += height;
}

// 找到结束索引
currentOffset = this.getOffsetTop(startIndex);
for (let i = startIndex; i < this.data.length; i++) {
const height = this.heightMap.get(i) || this.estimatedHeight;
if (currentOffset + height > scrollTop + clientHeight) {
endIndex = i;
break;
}
currentOffset += height;
endIndex = i;
}

// 增加缓冲项
startIndex = Math.max(0, startIndex - this.buffer);
endIndex = Math.min(this.data.length - 1, endIndex + this.buffer);
return { startIndex, endIndex };
}

// 重写:渲染后测量真实高度并缓存
renderVisibleItems() {
const { startIndex, endIndex } = this.getVisibleRange();
const visibleData = this.data.slice(startIndex, endIndex + 1);

// 计算偏移量(基于真实高度的累计值)
const offsetTop = this.getOffsetTop(startIndex);
this.visibleItems.style.transform = `translateY(${offsetTop}px)`;

// 渲染可见项
this.visibleItems.innerHTML = visibleData
.map((item, index) => `
<div class="list-item" data-index="${startIndex + index}">
${item.content}
</div>
`)
.join('');

// 测量并缓存真实高度
this.cacheRealHeights(startIndex, endIndex);

// 更新占位容器总高度(用已缓存的真实高度 + 剩余项的预估高度)
this.updateTotalHeight();
}

// 测量并缓存真实高度
cacheRealHeights(startIndex, endIndex) {
const items = this.visibleItems.querySelectorAll('.list-item');
items.forEach((item, index) => {
const actualIndex = startIndex + index;
const height = item.offsetHeight;
this.heightMap.set(actualIndex, height); // 缓存真实高度
});
}

// 更新总高度
updateTotalHeight() {
const totalHeight = this.getOffsetTop(this.data.length);
this.placeholder.style.height = `${totalHeight}px`;
}
}

四、虚拟列表的适用场景与注意事项

适用场景

  • 大数据列表(如表格、聊天记录、商品列表,数据量>1000条);
  • 移动端长列表(性能敏感,避免DOM过载);
  • 无限滚动场景(滚动到底部加载更多数据)。

注意事项

  1. 缓冲项(Buffer)设置:可视区域外多渲染3~5项,避免快速滚动时出现空白;
  2. 滚动节流:用 requestAnimationFrame 或节流函数限制渲染频率(如100ms/次);
  3. 动态高度优化
    • 预估高度尽量接近真实高度(减少修正次数);
    • 避免频繁修改已渲染项的高度(可能导致缓存失效);
  4. 复杂项优化:若列表项包含图片等异步资源,需监听加载完成后重新测量高度。

总结

虚拟列表通过“按需渲染”解决长列表性能问题,固定高度实现简单(适合高度统一场景),动态高度需结合“预估+缓存”处理高度差异(适合复杂内容)。核心是通过精准计算可视范围和偏移量,在用户无感知的情况下,仅渲染必要的DOM节点,平衡性能与体验。实际开发中,可直接使用成熟库(如 react-virtualizedvue-virtual-scroller)避免重复造轮子。

21.如何优化前端框架(Vue/React)的性能?(如组件缓存、路由懒加载、减少状态更新、使用 memo)

前端框架(Vue/React)的性能优化核心是减少不必要的渲染(重绘/重排)降低初始加载体积优化状态更新效率。由于Vue和React的设计理念不同,优化策略既有共通点,也有框架特有的实现方式。以下从通用策略和框架特有方案两方面展开:

一、通用优化策略(Vue/React 共通)

这些策略不依赖具体框架,核心是通过“减少资源体积”和“优化执行效率”提升性能。

1. 路由懒加载(减少初始加载体积)

前端框架通常通过路由管理页面,初始加载时若一次性加载所有路由组件,会导致JS bundle体积过大,加载缓慢。路由懒加载通过“按需加载”仅加载当前页面所需组件,显著减少首屏加载时间。

  • Vue 实现(基于 Vue Router):
    () => import() 动态导入组件,配合 webpack 自动拆分代码:

    // router/index.js
    const Home = () => import('@/views/Home.vue'); // 懒加载Home组件
    const About = () => import(/* webpackChunkName: "about" */ '@/views/About.vue'); // 自定义chunk名称
    
    const routes = [
      { path: '/', component: Home },
      { path: '/about', component: About }
    ];
    
  • React 实现(基于 React Router + React.lazy):
    React.lazy 动态导入组件,Suspense 处理加载状态:

    // App.jsx
    import { Suspense, lazy } from 'react';
    import { BrowserRouter, Routes, Route } from 'react-router-dom';
    
    // 懒加载路由组件
    const Home = lazy(() => import('./views/Home'));
    const About = lazy(() => import('./views/About'));
    
    function App() {
      return (
        <BrowserRouter>
          {/* 加载时显示占位内容 */}
          <Suspense fallback={<div>Loading...</div>}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/about" element={<About />} />
            </Routes>
          </Suspense>
        </BrowserRouter>
      );
    }
    

2. 长列表优化(避免大量DOM节点)

当列表数据量过大(如1000+条)时,即使框架有虚拟DOM优化,大量DOM节点仍会导致渲染卡顿。需用虚拟列表仅渲染可视区域内的项。

  • Vue 推荐库vue-virtual-scroller

    <template>
      <virtual-scroller
        :items="largeList"
        :item-height="50"
        class="scroller"
      >
        <template #item="{ item }">
          <div class="list-item">{{ item.content }}</div>
        </template>
      </virtual-scroller>
    </template>
    <script>
      import { VirtualScroller } from 'vue-virtual-scroller';
      import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
      export default {
        components: { VirtualScroller },
        data() {
          return { largeList: Array.from({ length: 10000 }, (_, i) => ({ content: `Item ${i}` })) };
        }
      };
    </script>
    
  • React 推荐库react-windowreact-virtualized

    import { FixedSizeList } from 'react-window';
    
    function LargeList() {
      // 渲染10000条数据,仅显示可视区域内的项
      return (
        <FixedSizeList
          height={400} // 可视区域高度
          width="100%"
          itemCount={10000} // 总条数
          itemSize={50} // 每项高度
        >
          {({ index, style }) => (
            <div style={style} className="list-item">
              Item {index}
            </div>
          )}
        </FixedSizeList>
      );
    }
    

3. 图片与资源优化(减少加载压力)

  • 懒加载图片:仅当图片进入可视区域时才加载,减少初始请求。
    • Vue 可用 v-lazy(需安装 vue-lazyload 插件);
    • React 可用 react-lazyload 或原生 loading="lazy" 属性。
  • 使用适当格式:优先用 WebP/AVIF(体积比JPG小30%+),配合 srcset 适配不同设备分辨率。

二、Vue 特有性能优化

Vue 基于“响应式系统”实现视图更新,优化核心是减少不必要的响应式追踪组件重渲染

1. 组件缓存(keep-alive

Vue 中组件默认每次切换(如路由切换、v-if 切换)都会销毁并重新创建,导致重复渲染。keep-alive 可缓存组件实例,避免重复初始化和DOM操作。

  • 基础用法

    <template>
      <!-- 缓存所有被包裹的组件 -->
      <keep-alive>
        <router-view /> <!-- 路由组件切换时会被缓存 -->
      </keep-alive>
    </template>
    
  • 精细控制
    通过 include/exclude 指定缓存/不缓存的组件(基于组件 name 属性):

    <keep-alive include="Home,About" exclude="Login">
      <router-view />
    </keep-alive>
    
  • 缓存生命周期
    被缓存的组件会触发 activated(激活时)和 deactivated(失活时)钩子,替代 mounted/unmounted

2. 减少响应式数据(避免不必要的追踪)

Vue 的响应式系统(Object.defineProperty/Vue3 Proxy)会对数据进行劫持,过多响应式数据会增加内存占用和更新开销。

  • 冻结静态数据:对不修改的数据用 Object.freeze 冻结,避免响应式追踪:

    data() {
      return {
        // 静态列表(不会修改),冻结后不触发响应式
        staticList: Object.freeze([{ id: 1, name: '静态项' }])
      };
    }
    
  • 拆分响应式数据:将频繁更新和不频繁更新的数据分开,避免“牵一发而动全身”:

    // 不推荐:所有数据放一个对象,更新一个字段会触发整个对象的依赖更新
    data() {
      return {
        user: { name: '张三', age: 20, address: '...' } // 地址很少更新,却和name/age一起被追踪
      };
    }
    
    // 推荐:拆分后,address更新不影响name/age的依赖
    data() {
      return {
        userInfo: { name: '张三', age: 20 },
        userAddress: '...' // 单独的响应式数据
      };
    }
    

3. 优化计算属性(computed)与侦听器(watch

  • **优先用 computed 而非 method**:computed 会缓存计算结果,仅依赖数据变化时才重新计算;method 每次渲染都会执行。

  • watch 避免深度监听deep: true 会递归遍历对象,开销大。如需监听对象某字段,直接指定路径:

    watch: {
      // 好:仅监听user.name,不深度遍历整个user
      'user.name'(newVal) { /* 处理逻辑 */ }
      // 差:深度监听整个user,即使无关字段变化也触发
      // user: { handler() {}, deep: true }
    }
    

三、React 特有性能优化

React 基于“状态驱动”实现更新,优化核心是减少不必要的组件重渲染(尤其是父组件更新导致子组件无辜重渲染)。

1. 缓存组件与函数(memo/useCallback/useMemo

React 函数组件默认每次父组件重渲染时都会重新执行,即使 props 未变化。需通过以下API避免无效重渲染:

  • **React.memo**:缓存组件,仅当 props 变化时才重渲染(类似类组件的 PureComponent):

    // 子组件:用memo包裹,避免父组件更新时无辜重渲染
    const Child = React.memo(({ name, onClick }) => {
      console.log('Child 渲染');
      return <button onClick={onClick}>{name}</button>;
    });
    
    // 父组件
    function Parent() {
      const [count, setCount] = useState(0);
      const name = '按钮';
    
      // 注意:若传递函数,需配合useCallback,否则每次渲染会生成新函数,导致Child重渲染
      const handleClick = useCallback(() => {
        console.log('点击');
      }, []); // 依赖为空,函数永久缓存
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>Count: {count}</button>
          <Child name={name} onClick={handleClick} />
        </div>
      );
    }
    
  • **useCallback**:缓存函数引用,避免因函数重新创建导致子组件 memo 失效(如上例)。

  • **useMemo**:缓存计算结果,避免每次渲染重复执行 expensive 计算:

    function ExpensiveComponent({ list }) {
      // 缓存计算结果:仅list变化时才重新计算
      const sortedList = useMemo(() => {
        return list.sort((a, b) => a.value - b.value); // 假设是耗时排序
      }, [list]); // 依赖list
    
      return <div>{sortedList.map(item => item.name)}</div>;
    }
    

2. 减少状态层级(避免状态过于集中)

React 中状态更新会触发当前组件及所有子组件的重渲染(除非被 memo 阻止)。若状态层级过深或过于集中,会导致大量组件无辜重渲染。

  • 拆分状态:将状态下放到使用它的子组件,而非集中在顶层父组件:

    // 不推荐:顶层集中所有状态,任何状态变化都会导致整个组件树重渲染
    function App() {
      const [user, setUser] = useState({});
      const [cart, setCart] = useState([]);
      const [theme, setTheme] = useState('light');
      // ... 大量状态
      return (
        <div>
          <Header user={user} theme={theme} />
          <Cart cart={cart} />
          {/* ... */}
        </div>
      );
    }
    
    // 推荐:状态下放到子组件,各自维护自己的状态
    function Header() {
      const [theme, setTheme] = useState('light'); // 仅Header使用的状态
      return <div>{/* 仅Header依赖theme,更新时仅Header重渲染 */}</div>;
    }
    
  • 使用状态管理库拆分状态:若需跨组件共享状态,用 Redux Toolkit(React)的 createSlice 或 Pinia(Vue)的 defineStore 按领域拆分状态,避免“一更新全更新”。

3. 避免不必要的渲染触发(key 优化)

React 依赖 key 识别列表项的身份,key 不稳定会导致组件频繁销毁重建(而非更新),浪费性能。

  • key 需稳定且唯一:用数据的唯一ID(如 item.id)而非索引(index)作为 key

    // 差:用index作key,列表增删时key会变化,导致组件重建
    {list.map((item, index) => <Item key={index} data={item} />)}
    
    // 好:用唯一ID作key,即使顺序变化,key仍稳定
    {list.map(item => <Item key={item.id} data={item} />)}
    

四、框架无关的进阶优化

  1. 使用 Web Workers 处理计算密集型任务
    复杂计算(如数据解析、图表渲染)放在 Web Workers 中执行,避免阻塞主线程(框架渲染依赖主线程)。

  2. 服务端渲染(SSR)/静态站点生成(SSG)

    • Vue 用 Nuxt.js,React 用 Next.js,通过服务端预渲染HTML,提升首屏加载速度(减少白屏时间)。
  3. 性能监测与针对性优化

    • Vue 用 Vue Devtools 的“性能”面板,检测组件重渲染频率和耗时;
    • React 用 React Devtools 的“Profiler”,记录渲染时间线,定位慢组件。

总结

Vue/React 性能优化的核心逻辑是:

  • 加载阶段:通过路由懒加载、资源压缩减少初始体积;
  • 运行阶段:通过组件缓存、减少无效重渲染(Vue keep-alive / React memo)提升执行效率;
  • 数据处理:拆分状态、避免过度响应式追踪,降低更新成本。

需结合具体业务场景(如长列表、高频更新组件)和性能监测工具,优先优化“用户感知明显”的瓶颈(如首屏加载、滚动卡顿)。

22.什么是代码分割?代码分割的方式有哪些(如路由分割、组件分割、动态 import)?

一、代码分割(Code Splitting)的定义

代码分割是前端性能优化的核心技术,指将应用的代码拆分成多个独立的“代码块(Chunk)”,然后按需加载(仅加载当前页面/功能所需的代码),而非一次性加载全部代码。

其核心价值在于:

  • 减少初始加载体积:避免用户首次访问时加载冗余代码(如未访问的路由、未触发的功能),缩短首屏加载时间;
  • 优化资源利用:仅在需要时加载对应代码,节省带宽(尤其移动端)和浏览器解析时间;
  • 提升用户体验:首屏更快呈现,后续操作按需加载,避免长时间白屏。

二、代码分割的实现方式(基于动态 import()

现代前端打包工具(Webpack、Vite、Rollup 等)均支持代码分割,核心依赖 ES6 动态 import() 语法(返回 Promise,异步加载模块)。工具会自动将动态导入的模块拆分为独立的代码块(Chunk),并在需要时通过网络请求加载。

常见的分割方式按“拆分粒度”和“加载时机”可分为以下 3 类:

1. 路由分割(最常用,按页面拆分)

路由是代码分割的最佳场景:不同路由对应不同页面,用户同一时间只会访问一个路由,因此可将每个路由的组件拆分为独立代码块,仅在跳转时加载。

适用场景:单页应用(SPA)的多路由页面(如首页、详情页、个人中心)。

实现示例

  • Vue(结合 Vue Router)

    // router/index.js
    import { createRouter, createWebHistory } from 'vue-router';
    
    // 动态导入路由组件:每个路由对应一个独立Chunk
    const Home = () => import('@/views/Home.vue'); // 首页Chunk
    const Detail = () => import(/* webpackChunkName: "detail" */ '@/views/Detail.vue'); // 自定义Chunk名称
    const User = () => import('@/views/User.vue'); // 个人中心Chunk
    
    const routes = [
      { path: '/', component: Home },
      { path: '/detail/:id', component: Detail },
      { path: '/user', component: User }
    ];
    
    const router = createRouter({
      history: createWebHistory(),
      routes
    });
    
  • React(结合 React Router + React.lazy)

    // App.jsx
    import { Suspense, lazy } from 'react';
    import { BrowserRouter, Routes, Route } from 'react-router-dom';
    
    // 动态导入路由组件(React.lazy 包装动态import)
    const Home = lazy(() => import('./views/Home')); // 首页Chunk
    const Detail = lazy(() => import('./views/Detail')); // 详情页Chunk
    const User = lazy(() => import('./views/User')); // 个人中心Chunk
    
    function App() {
      return (
        <BrowserRouter>
          {/* Suspense:指定加载时的占位UI(如Loading) */}
          <Suspense fallback={<div>加载中...</div>}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/detail/:id" element={<Detail />} />
              <Route path="/user" element={<User />} />
            </Routes>
          </Suspense>
        </BrowserRouter>
      );
    }
    

2. 组件分割(按功能组件拆分)

对于应用中“不常用的组件”(如弹窗、模态框、复杂表单),无需在初始加载时加载,可在用户触发特定操作(如点击按钮)时再动态导入,进一步减小主包体积。

适用场景:点击显示的弹窗(Modal)、条件渲染的复杂组件(如筛选器、编辑器)。

实现示例

  • Vue 组件分割

    <template>
      <div>
        <button @click="showModal = true">打开弹窗</button>
        <!-- 条件渲染动态导入的组件 -->
        <Modal v-if="showModal" @close="showModal = false" />
      </div>
    </template>
    
    <script setup>
    import { ref, defineAsyncComponent } from 'vue';
    
    // 动态导入弹窗组件(仅在showModal为true时加载)
    const Modal = defineAsyncComponent(() => import('@/components/Modal.vue'));
    const showModal = ref(false);
    </script>
    
  • React 组件分割

    import { useState, lazy, Suspense } from 'react';
    
    // 动态导入弹窗组件(React.lazy 包装)
    const Modal = lazy(() => import('./components/Modal'));
    
    function App() {
      const [showModal, setShowModal] = useState(false);
    
      return (
        <div>
          <button onClick={() => setShowModal(true)}>打开弹窗</button>
          {showModal && (
            // 用Suspense处理加载状态
            <Suspense fallback={<div>加载弹窗中...</div>}>
              <Modal onClose={() => setShowModal(false)} />
            </Suspense>
          )}
        </div>
      );
    }
    

3. 基于条件的分割(按业务逻辑拆分)

根据用户行为、权限或环境(如移动端/PC端),动态加载不同的功能模块(如不同角色的操作权限组件、不同设备的适配代码)。

适用场景:权限控制(如管理员/普通用户加载不同功能)、多环境适配(如H5/桌面端加载不同组件)。

实现示例

// 基于用户角色动态加载权限组件
async function loadPermissionComponent(role) {
  let Component;
  if (role === 'admin') {
    // 管理员加载完整权限组件
    const AdminComp = await import('./components/AdminPermission');
    Component = AdminComp.default;
  } else {
    // 普通用户加载基础权限组件
    const UserComp = await import('./components/UserPermission');
    Component = UserComp.default;
  }
  return Component;
}

// 在组件中使用
function PermissionWrapper({ userRole }) {
  const [Component, setComponent] = useState(null);

  useEffect(() => {
    // 组件挂载后,根据角色加载对应组件
    loadPermissionComponent(userRole).then(comp => setComponent(comp));
  }, [userRole]);

  if (!Component) return <div>加载权限组件中...</div>;
  return <Component />;
}

三、代码分割的关键细节

  1. 打包工具的自动处理
    Webpack、Vite 等会自动识别 import() 语法,将动态导入的模块拆分为独立 Chunk(默认以数字命名,可通过 /* webpackChunkName: "name" */ 自定义名称),无需手动配置。

  2. 加载状态处理
    动态导入是异步操作,需处理“加载中”“加载失败”状态:

    • Vue 用 defineAsyncComponentloadingComponenterrorComponent
    • React 用 Suspense(加载中)和错误边界(Error Boundary,加载失败)。
  3. 避免过度分割
    分割粒度并非越小越好:过多的 Chunk 会导致网络请求数量增加(每个 Chunk 需一次请求),反而影响性能。建议按“页面”或“功能模块”拆分,而非单个小组件。

总结

代码分割的核心是通过动态 import() 语法,将代码按“路由”“组件”或“业务条件”拆分为独立 Chunk,实现“按需加载”。其本质是用“时间换空间”——牺牲少量后续操作的加载时间,换取首屏加载的快速呈现,是大型应用性能优化的必备手段。实际开发中,路由分割是投入产出比最高的方式,建议优先实现。

23.如何优化首屏加载速度?(如骨架屏、预加载、懒加载、减少首屏资源体积)

首屏加载速度是用户对产品的“第一印象”,直接影响用户留存率(研究表明,首屏加载超过3秒,用户流失率可达50%以上)。优化核心是“减少首屏资源体积”“加速资源加载”“优化用户感知体验”,具体策略如下:

一、减少首屏资源体积(从源头降低加载压力)

首屏加载的资源(JS、CSS、图片等)体积越小,传输和解析速度越快,这是优化的根基。

1. 代码层面:精简首屏必要代码

  • 代码分割(按需加载)
    仅加载首屏必需的代码,非首屏代码(如其他路由、隐藏组件)通过动态import()延迟加载(详见“代码分割”章节)。
    例:React/Vue的路由懒加载,确保首屏JS体积减少60%+。

  • 移除未使用代码(Tree Shaking)
    借助Webpack、Vite等工具的Tree Shaking功能,自动删除未被引用的代码(需使用ES6模块import/export)。
    Webpack配置示例:

    // webpack.config.js
    module.exports = {
      mode: 'production', // 生产环境自动启用Tree Shaking
      optimization: {
        usedExports: true, // 标记未使用的导出
        minimize: true // 配合Terser删除未使用代码
      }
    };
    
  • 压缩代码(Minification)
    通过工具压缩JS/CSS,移除空格、注释、简化变量名(如Terser压缩JS,CSSNano压缩CSS),通常可减少30%~50%体积。
    现代打包工具(Webpack/Vite)在生产模式下默认开启。

2. 资源层面:优化图片与静态资源

  • 使用现代图片格式
    用WebP/AVIF替代JPG/PNG,相同质量下体积减少30%~70%。
    例:<img src="image.webp" alt="示例图" />(兼容旧浏览器可降级为JPG:<picture><source srcset="image.webp" type="image/webp"><img src="image.jpg"></picture>)。

  • 图片压缩与裁剪

    • 首屏图片按需裁剪(如移动端用300px宽,PC端用600px宽),避免“大图小用”;
    • 用工具(如TinyPNG、Squoosh)压缩图片,保留视觉质量的同时减小体积。
  • 字体优化

    • 仅加载首屏必需的字体子集(如只包含“0-9、a-z”),而非完整字体库;

    • font-display: swap避免字体加载期间的文本不可见(FOIT):

      @font-face {
        font-family: 'MyFont';
        src: url('myfont.woff2') format('woff2');
        font-display: swap; /* 字体加载中显示默认字体,加载完成后替换 */
      }
      

二、加速资源加载(优化网络传输效率)

即使资源体积小,若加载链路长、优先级低,仍会拖慢首屏速度。需通过网络策略和加载优先级优化加速。

1. 优化关键资源加载优先级

首屏渲染依赖的“关键资源”(如核心JS、首屏CSS、Logo图片)需优先加载,非关键资源(如广告JS、非首屏图片)延迟加载。

  • 预加载关键资源(Preload)
    <link rel="preload">强制浏览器优先加载关键资源(优先级高于普通请求):

    <!-- 预加载首屏核心JS -->
    <link rel="preload" href="/js/main.js" as="script" />
    <!-- 预加载首屏关键图片 -->
    <link rel="preload" href="/images/logo.webp" as="image" />
    
  • 内联关键CSS
    将首屏渲染必需的CSS(如导航栏、Banner样式)直接内联到<style>标签,避免额外的CSS文件请求阻塞渲染(CSS会阻塞DOM解析和渲染)。
    非首屏CSS(如footer、弹窗样式)通过<link>异步加载:

    <head>
      <!-- 内联首屏关键CSS(体积控制在15KB内,避免HTML过大) -->
      <style>
        .header { ... } /* 首屏导航样式 */
        .banner { ... } /* 首屏Banner样式 */
      </style>
      <!-- 异步加载非首屏CSS -->
      <link rel="stylesheet" href="/css/other.css" media="print" onload="this.media='all'" />
    </head>
    
  • 延迟加载非关键JS
    deferasync标记非首屏JS(如统计脚本、广告脚本),避免阻塞DOM解析:

    • async:下载完成后立即执行(顺序不确定);
    • defer:下载完成后等待DOM解析完成再执行(按顺序执行)。
    <script src="/js/analytics.js" async></script> <!-- 统计脚本,不影响首屏 -->
    <script src="/js/chat.js" defer></script> <!-- 聊天脚本,延迟执行 -->
    

2. 优化网络传输链路

  • 启用HTTP/2或HTTP/3
    相比HTTP/1.1,HTTP/2支持多路复用(同一连接并行传输多个资源,避免队头阻塞),可减少30%+加载时间;HTTP/3基于QUIC协议,进一步优化弱网环境下的传输稳定性。

  • 使用CDN分发资源
    CDN(内容分发网络)将资源缓存到用户就近的节点,减少跨地域传输延迟(如北京用户从北京节点加载,而非美国服务器)。

  • 启用Gzip/Brotli压缩
    服务器对JS、CSS、HTML等文本资源启用压缩(Brotli比Gzip压缩率高15%~20%),减少传输体积。
    Nginx配置示例(启用Brotli):

    http {
      brotli on;
      brotli_types text/css application/javascript text/html; # 压缩指定类型
    }
    

3. 利用缓存减少重复请求

  • HTTP缓存:对首屏静态资源(如JS、CSS、图片)配置强缓存(Cache-Control: max-age=31536000),用户二次访问时直接从本地缓存加载(详见“HTTP缓存”章节)。
  • Service Worker预缓存:通过PWA的Service Worker在首次加载后缓存首屏资源,下次访问(甚至离线)直接从缓存读取,实现“秒开”。

三、优化用户感知体验(减少“等待感”)

即使实际加载时间未变,通过优化用户感知(如显示加载状态、减少白屏),可显著提升用户体验。

1. 骨架屏(Skeleton Screen)

在首屏内容加载完成前,显示与最终页面结构一致的“灰色占位骨架”(如导航栏骨架、列表项骨架),让用户感知到“页面正在加载”,而非白屏。

  • 实现方式

    • 简单场景:直接在HTML中嵌入骨架屏HTML/CSS(无需JS,首屏即显示);
    • 框架场景:Vue/React通过组件实现,如vue-skeleton-webpack-plugin或手动编写骨架组件。

    示例(简易HTML骨架屏):

    <!-- 骨架屏:在<body>顶部,内容加载后隐藏 -->
    <div class="skeleton" id="skeleton">
      <div class="skeleton-header"></div> <!-- 导航栏骨架 -->
      <div class="skeleton-banner"></div> <!-- Banner骨架 -->
      <div class="skeleton-list"> <!-- 列表骨架 -->
        <div class="skeleton-item"></div>
        <div class="skeleton-item"></div>
      </div>
    </div>
    
    <style>
      .skeleton { position: absolute; top: 0; left: 0; width: 100%; }
      .skeleton-header { height: 60px; background: #eee; }
      .skeleton-banner { height: 200px; background: #eee; margin: 10px 0; }
      .skeleton-item { height: 80px; background: #eee; margin: 10px 0; border-radius: 4px; }
    </style>
    
    <script>
      // 内容加载完成后隐藏骨架屏
      window.addEventListener('load', () => {
        document.getElementById('skeleton').style.display = 'none';
      });
    </script>
    

2. 预连接与DNS预解析

对首屏需要访问的第三方域名(如CDN、API服务器),提前进行DNS解析、TCP握手,减少后续请求的延迟:

<!-- 预解析DNS -->
<link rel="dns-prefetch" href="https://api.example.com" />
<!-- 预连接(DNS+TCP+TLS,适合HTTPS域名) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin />

3. 懒加载非首屏资源

  • 图片懒加载:仅当图片进入可视区域时才加载,减少首屏请求数。
    现代浏览器支持原生loading="lazy"

    <img src="image.jpg" alt="非首屏图片" loading="lazy" />
    

    旧浏览器可用JS实现(监听scroll事件,判断图片位置)。

  • 组件懒加载:首屏外的组件(如弹窗、底部评论区)通过动态import()延迟加载,避免初始渲染压力。

四、首屏优化优先级与检测工具

优先级排序(从高到低)

  1. 减少首屏关键资源体积(代码分割、图片优化);
  2. 优化关键资源加载(内联CSS、Preload);
  3. 启用缓存与CDN;
  4. 优化用户感知(骨架屏);
  5. 网络协议升级(HTTP/2/3)。

检测工具

  • Lighthouse(Chrome开发者工具内置):生成首屏性能评分,指出具体优化点;
  • WebPageTest:检测全球各地加载速度,分析 waterfall 瀑布流(资源加载顺序与耗时);
  • Chrome Network面板:模拟弱网环境(如3G),观察首屏加载瓶颈。

总结

首屏加载优化是“技术优化”与“用户感知优化”的结合:

  • 技术层面:通过减小体积、加速传输、利用缓存,从根本上缩短加载时间;
  • 感知层面:通过骨架屏、预加载提示,减少用户等待焦虑。

实际优化中,需结合业务场景(如电商首屏需优先加载商品列表,资讯类优先加载标题),通过工具定位瓶颈,针对性优化。

24.什么是骨架屏?骨架屏的作用是什么?如何实现骨架屏(手动编写、自动生成)?

一、骨架屏(Skeleton Screen)的定义

骨架屏是页面加载过程中显示的“占位UI”,其结构与最终渲染的页面布局一致(如导航栏、列表项、图片框的灰色占位块),但仅包含基础样式(无实际内容)。它会在页面核心资源(JS、数据、图片)加载完成后,被真实内容替换。

简单说,骨架屏是“页面加载中的低保真预览版”,替代传统的空白页或加载动画(如 spinner)。

二、骨架屏的核心作用

骨架屏的核心价值是优化用户对“加载等待”的感知,解决以下问题:

  1. 减少“白屏焦虑”
    传统加载方式中,用户可能面对几秒的空白页,误以为页面“卡死”或“无法访问”。骨架屏通过提前展示页面结构,让用户感知到“页面正在加载,且即将完成”,降低跳出率。

  2. 提升感知性能
    即使实际加载时间不变,骨架屏让用户觉得“加载更快”——因为视觉上有内容(而非空白),且骨架到真实内容的过渡更自然(心理学上的“进展感知”)。

  3. 保持页面上下文
    骨架屏与最终页面结构一致(如列表项位置、按钮布局),用户可提前预判内容位置,加载完成后无需重新适应布局。

三、骨架屏的实现方式(手动编写 vs 自动生成)

根据项目复杂度和维护成本,骨架屏的实现分为“手动编写”(适合简单场景)和“自动生成”(适合复杂项目)两类。

1. 手动编写骨架屏(基础且灵活)

手动编写是最直接的方式:在HTML中嵌入骨架屏的结构和样式,页面加载完成后隐藏骨架屏,显示真实内容。

核心步骤

  • 分析首屏结构,用HTML+CSS模拟关键区域(如导航栏、图片框、文本块);
  • 将骨架屏代码放在<body>最顶部,确保优先渲染;
  • 监听页面加载完成事件(如window.onload),隐藏骨架屏,显示真实内容。

代码示例(通用HTML/CSS实现)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>骨架屏示例</title>
  <style>
    /* 骨架屏样式 */
    .skeleton-container {
      position: fixed; /* 覆盖整个屏幕 */
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: #fff;
      z-index: 9999; /* 确保在最上层 */
    }
    .skeleton-header {
      height: 60px;
      background: #f5f5f5; /* 灰色占位 */
      border-bottom: 1px solid #eee;
    }
    .skeleton-banner {
      height: 200px;
      margin: 10px;
      background: #f5f5f5;
      border-radius: 4px;
    }
    .skeleton-list {
      padding: 10px;
    }
    .skeleton-item {
      height: 80px;
      margin-bottom: 10px;
      background: #f5f5f5;
      border-radius: 4px;
      /* 增加“闪烁动画”,提示用户“正在加载” */
      animation: skeleton-loading 1.5s infinite;
    }
    @keyframes skeleton-loading {
      0% { background-position: -200px 0; }
      100% { background-position: 200px 0; }
    }

    /* 真实内容默认隐藏 */
    .real-content {
      display: none;
    }
  </style>
</head>
<body>
  <!-- 1. 骨架屏(优先渲染) -->
  <div class="skeleton-container" id="skeleton">
    <div class="skeleton-header"></div>
    <div class="skeleton-banner"></div>
    <div class="skeleton-list">
      <div class="skeleton-item"></div>
      <div class="skeleton-item"></div>
      <div class="skeleton-item"></div>
    </div>
  </div>

  <!-- 2. 真实内容(默认隐藏) -->
  <div class="real-content" id="realContent">
    <header class="header">真实导航栏</header>
    <div class="banner"><img src="banner.jpg" alt="轮播图"></div>
    <ul class="list">
      <li>真实列表项1</li>
      <li>真实列表项2</li>
      <li>真实列表项3</li>
    </ul>
  </div>

  <script>
    // 3. 页面加载完成后,隐藏骨架屏,显示真实内容
    window.addEventListener('load', () => {
      document.getElementById('skeleton').style.display = 'none';
      document.getElementById('realContent').style.display = 'block';
    });

    // 可选:设置超时兜底(避免加载失败时骨架屏一直显示)
    setTimeout(() => {
      document.getElementById('skeleton').style.display = 'none';
      document.getElementById('realContent').style.display = 'block';
    }, 5000); // 5秒后强制切换
  </script>
</body>
</html>

手动编写的优缺点

  • 优点:灵活可控(可精确匹配设计)、无额外依赖、性能开销低;
  • 缺点:维护成本高(页面结构变化时需同步修改骨架屏)、不适合复杂页面(如动态渲染的列表)。

2. 自动生成骨架屏(适合复杂项目)

对于结构复杂或动态变化的页面(如电商商品列表、新闻流),手动编写效率低,可通过工具自动生成骨架屏。核心原理是“分析页面结构,自动生成匹配的占位UI”。

常用工具与实现方式

(1)基于Webpack插件(适合Vue/React项目)
  • Vue项目vue-skeleton-webpack-plugin
    原理:通过配置文件定义骨架屏组件,Webpack打包时将骨架屏注入HTML,实现首屏优先渲染。

    示例配置:

    // webpack.config.js
    const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
    
    module.exports = {
      plugins: [
        new SkeletonWebpackPlugin({
          // 骨架屏入口文件(Vue组件)
          webpackConfig: {
            entry: {
              app: path.join(__dirname, './src/skeleton.js')
            }
          },
          // 针对不同路由生成不同骨架屏
          router: {
            mode: 'history',
            routes: [
              { path: '/', skeletonId: 'home-skeleton' }, // 首页骨架屏
              { path: '/detail', skeletonId: 'detail-skeleton' } // 详情页骨架屏
            ]
          }
        })
      ]
    };
    
  • React项目react-skeleton-loader@loadable/component 结合骨架屏组件
    原理:通过高阶组件包装页面,加载期间显示自动生成的骨架屏。

(2)基于组件库(声明式生成)
  • **react-content-loader**(React专用):
    通过组件 props 声明骨架屏结构(如列表、卡片),自动生成SVG或CSS骨架。

    示例:

    import ContentLoader from 'react-content-loader';
    
    // 自动生成列表项骨架屏
    const ListSkeleton = () => (
      <ContentLoader 
        speed={2}
        width={400}
        height={100}
        backgroundColor="#f5f5f5"
        foregroundColor="#e0e0e0"
      >
        <rect x="0" y="0" width="400" height="100" /> {/* 列表项背景 */}
        <rect x="10" y="10" width="30" height="30" borderRadius="50%" /> {/* 头像占位 */}
        <rect x="50" y="15" width="200" height="10" /> {/* 标题占位 */}
      </ContentLoader>
    );
    
    // 使用:加载期间显示骨架屏
    function ListPage() {
      const [data, setData] = useState(null);
      useEffect(() => {
        fetchData().then(res => setData(res)); // 异步加载数据
      }, []);
    
      return data ? <RealList data={data} /> : <ListSkeleton />;
    }
    
(3)基于页面截图分析(通用方案)

工具如 page-skeleton-webpack-plugin(已停止维护,但思路经典):

  1. 自动打开浏览器渲染页面;
  2. 分析DOM结构和样式(如宽高、位置、圆角);
  3. 生成匹配的骨架屏HTML/CSS;
  4. 注入到打包产物中,实现首屏展示。

自动生成的优缺点

  • 优点:适合复杂页面、减少手动维护成本、可动态适配页面结构变化;
  • 缺点:需引入工具依赖、生成的骨架屏可能不够精细(需手动调整)、部分工具对动态内容支持有限。

四、骨架屏的最佳实践

  1. 仅在首屏使用:非首屏内容(如滚动后加载的区域)无需骨架屏,避免浪费资源;
  2. 保持样式一致性:骨架屏的布局、间距、圆角需与真实页面一致,避免切换时的“跳跃感”;
  3. 添加加载动画:通过渐变或闪烁动画(如@keyframes)提示用户“正在加载”,避免骨架屏被误认为是真实内容;
  4. 控制骨架屏体积:手动编写时,骨架屏CSS体积建议控制在10KB内,避免增加HTML加载时间;
  5. 适配响应式:骨架屏需通过媒体查询适配不同屏幕尺寸(如移动端/PC端布局不同)。

总结

骨架屏是“感知性能优化”的核心手段,通过提前展示页面结构,减少用户等待焦虑。实现方式需根据项目复杂度选择:简单页面用手动编写(灵活高效),复杂项目用自动生成工具(降低维护成本)。核心目标是让用户在加载过程中获得“页面正在有序加载”的明确感知,最终提升留存率。

25.什么是预加载(Preload)和预连接(Preconnect)?它们的作用是什么?如何使用?

一、预加载(Preload)与预连接(Preconnect)的定义

预加载(Preload)和预连接(Preconnect)是浏览器提供的资源加载优化机制,通过“提前准备”关键资源或连接,减少页面关键资源的加载延迟,提升首屏渲染速度。

  • 预加载(Preload):开发者明确告知浏览器“当前页面必需的关键资源”(如首屏CSS、核心JS、字体),让浏览器优先加载这些资源(不阻塞页面初步解析),在资源需要时能立即使用。
  • 预连接(Preconnect):提前与跨域域名(如CDN、第三方API服务器)建立完整连接(包括DNS解析、TCP握手、TLS加密协商),当后续需要请求该域名的资源时,可直接发送请求,省去连接建立的时间

二、核心作用:减少关键资源的加载延迟

现代网页加载的性能瓶颈常来自“资源请求时机过晚”或“连接建立耗时过长”。Preload和Preconnect通过“提前行动”解决这些问题:

1. 预加载(Preload)的作用

  • 提升关键资源优先级:浏览器默认的资源加载优先级由解析顺序决定(如<script><link>之后解析则优先级低),Preload强制将资源标记为“高优先级”,确保关键资源(如首屏CSS)不被低优先级资源(如广告图片)阻塞。
  • 避免“渲染阻塞”:对于非HTML解析时发现的关键资源(如动态加载的核心JS、字体文件),Preload可提前加载,避免在渲染关键时刻才请求导致的“白屏”或“无样式内容闪烁(FOUC)”。
  • 支持多种资源类型:可预加载JS、CSS、字体、图片、音频等,尤其适合解决字体加载延迟导致的“文本不可见(FOIT)”问题。

2. 预连接(Preconnect)的作用

  • 减少跨域连接耗时:跨域请求(如从example.com请求cdn.example.com的资源)需要经过“DNS解析→TCP握手→TLS协商”三步,总耗时可能达数百毫秒(尤其弱网环境)。Preconnect提前完成这三步,后续请求可直接发送,节省30%~50%的连接时间。
  • 优化第三方资源加载:对于依赖第三方服务的页面(如使用Google Fonts、百度统计的网站),Preconnect能显著减少第三方资源的加载延迟。

三、使用方法(HTML标签实现)

Preload和Preconnect均通过<link>标签实现,需放在<head>中,确保浏览器尽早解析。

1. 预加载(Preload)的使用

语法

<link rel="preload" href="资源URL" as="资源类型" [crossorigin] [onload="回调"]>
  • 核心属性
    • href:资源的URL(必填);
    • as:指定资源类型(必填),如script(JS)、style(CSS)、font(字体)、image(图片)等,浏览器会根据类型优化加载策略;
    • crossorigin:跨域资源需添加(如字体、跨域图片),否则可能加载失败;
    • onload:资源加载完成后的回调(如应用预加载的字体)。

常见场景示例

  • 预加载首屏核心JS

    <!-- 预加载首屏必需的JS,确保优先加载 -->
    <link rel="preload" href="/js/main.js" as="script">
    
  • 预加载关键CSS

    <!-- 预加载首屏CSS,避免渲染阻塞 -->
    <link rel="preload" href="/css/main.css" as="style" onload="this.rel='stylesheet'">
    

    onloadrelpreload改为stylesheet,确保CSS生效;兼容旧浏览器可加<link rel="stylesheet" href="/css/main.css" media="print" onload="this.media='all'">作为降级)

  • 预加载字体(解决FOIT)

    <!-- 预加载跨域字体,需加crossorigin -->
    <link rel="preload" href="https://cdn.example.com/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
    

2. 预连接(Preconnect)的使用

语法

<link rel="preconnect" href="跨域域名URL" [crossorigin]>
  • 核心属性
    • href:跨域域名的根URL(如https://cdn.example.com,无需具体资源路径);
    • crossorigin:跨域资源需要时添加(如CDN的字体、图片)。

常见场景示例

  • 预连接CDN域名

    <!-- 提前与CDN建立连接,后续请求CDN资源时直接使用 -->
    <link rel="preconnect" href="https://cdn.example.com" crossorigin>
    
  • 预连接第三方API域名

    <!-- 提前与支付API域名建立连接,优化支付按钮点击后的请求速度 -->
    <link rel="preconnect" href="https://api.payment.com">
    
  • 结合DNS预解析(dns-prefetch)降级
    对不支持Preconnect的旧浏览器(如IE),可先用dns-prefetch提前解析DNS:

    <link rel="preconnect" href="https://cdn.example.com" crossorigin>
    <link rel="dns-prefetch" href="https://cdn.example.com"> <!-- 降级方案 -->
    

四、注意事项(避免滥用)

  1. Preload:只用于“当前页面必需”的资源
    过度预加载会占用带宽,导致其他资源加载延迟。例如,非首屏的图片、其他路由的JS不应使用Preload(可用prefetch,低优先级预获取未来可能用到的资源)。

  2. Preconnect:只用于“确定会访问”的跨域域名
    每个Preconnect都会占用浏览器的连接池资源,对不确定是否会用到的域名(如可选的第三方插件),滥用会浪费资源。

  3. 避免与其他预加载机制混淆

    • preload vs prefetchpreload是高优先级,用于当前页面必需资源;prefetch是低优先级,用于未来页面(如用户可能点击的下一页)。
    • preload vs defer/asyncpreload仅负责加载资源,不自动执行;defer/async加载后会自动执行JS。

总结

Preload和Preconnect是“主动优化资源加载链路”的核心技术:

  • Preload通过“提前加载关键资源”确保首屏渲染时资源已就绪;
  • Preconnect通过“提前建立跨域连接”减少连接耗时。

合理使用可显著减少首屏加载时间(尤其弱网环境),但需避免滥用,否则会适得其反。实际应用中,优先对“首屏CSS、核心JS、CDN域名”使用这两种机制,配合Lighthouse等工具检测效果。

26.如何优化移动端前端性能?(如适配方案、触摸事件优化、避免过度动画)

移动端前端性能优化需针对移动设备的特性(如屏幕尺寸多样、硬件性能有限、网络环境复杂、触摸交互为主)进行针对性优化,核心目标是提升页面流畅度、减少加载时间、优化交互体验。以下从关键维度展开具体策略:

一、适配方案优化:兼顾显示效果与性能

移动端设备屏幕尺寸、分辨率差异大(从3.5英寸手机到10英寸平板),适配不当会导致布局错乱或资源浪费,间接影响性能。

1. 合理设置视口(Viewport)

通过meta:viewport控制页面缩放,避免浏览器默认缩放导致的布局错乱和额外渲染开销。

标准配置

<!-- 禁止缩放,宽度等于设备宽度,初始缩放1.0 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  • 作用:让页面宽度与设备宽度一致,避免浏览器对页面进行二次缩放计算,减少渲染压力;
  • 注意:user-scalable=no虽能禁用缩放(适合交互密集型应用),但可能影响无障碍访问,需根据业务权衡。

2. 高效的布局方案

避免使用复杂布局(如多层嵌套的浮动布局),优先选择性能更优的方案:

  • 弹性布局(Flexbox):替代浮动(float)和定位(position),减少布局计算复杂度,尤其适合移动端流式布局;

    .container {
      display: flex; /* 弹性布局,子元素自动分配空间 */
      flex-wrap: wrap; /* 超出换行,适配小屏幕 */
    }
    
  • 网格布局(Grid):适合二维布局(如商品网格),语义化更强,避免嵌套层级过深;

  • 响应式断点精简:移动端断点不宜过多(通常34个:<375px、375px768px、>768px),减少媒体查询(@media)的计算开销。

3. 动态单位适配:避免图片/元素变形

  • 使用remvw进行尺寸适配

    • rem:基于根元素字体大小(html { font-size: 16px; }),通过JS动态计算根字体大小(如按屏幕宽度比例),适合整体布局;

      // 动态设置rem基准值(屏幕宽度的1/10)
      function setRemUnit() {
        const docEl = document.documentElement;
        const rem = docEl.clientWidth / 10; // 1rem = 屏幕宽度的1/10
        docEl.style.fontSize = rem + 'px';
      }
      setRemUnit();
      window.addEventListener('resize', setRemUnit); // 屏幕变化时重新计算
      
    • vw:直接基于视口宽度(1vw = 视口宽度的1%),适合局部元素(如字体、间距),无需JS计算;

      .title {
        font-size: 5vw; /* 字体大小随屏幕宽度变化 */
        margin: 2vw 0; /* 间距随屏幕宽度变化 */
      }
      
  • 图片响应式适配:避免“大图小用”(如在375px屏幕加载1080px图片),通过srcsetsizes自动匹配合适尺寸:

    <img 
      src="image-375.jpg"  <!-- 默认图(小屏幕) -->
      srcset="image-375.jpg 375w, image-768.jpg 768w"  <!-- 不同尺寸图片及宽度标识 -->
      sizes="(min-width: 768px) 768px, 100vw"  <!-- 不同屏幕下的显示宽度 -->
      alt="响应式图片"
    >
    

二、触摸与交互优化:提升响应速度

移动端以触摸操作为主,触摸事件的延迟或卡顿会直接影响用户体验(如点击按钮无反应、滑动不流畅)。

1. 消除点击延迟(300ms延迟问题)

早期浏览器为判断“双击缩放”,会在触摸后等待300ms再触发click事件,导致点击响应延迟。

解决方案

  • 方案1:通过viewport设置禁用缩放(上文已提及),部分浏览器会取消300ms延迟;
  • 方案2:使用触摸事件(touchstart/touchend)替代click,但需处理“触摸穿透”问题(可通过pointer-events: none临时禁用下层元素交互);
  • 方案3:使用FastClick库(已过时,现代浏览器多已优化此问题,建议优先用原生方案)。

2. 优化触摸事件性能

触摸事件(如touchmovetouchstart)触发频率极高(每秒60次以上),若回调函数复杂,会阻塞主线程导致卡顿。

优化策略

  • 防抖/节流处理:对touchmove(如滑动手势)使用节流,限制执行频率(如每100ms执行一次);

    function throttle(fn, delay = 100) {
      let lastTime = 0;
      return function(...args) {
        const now = Date.now();
        if (now - lastTime > delay) {
          fn.apply(this, args);
          lastTime = now;
        }
      };
    }
    
    // 节流处理滑动事件
    document.addEventListener('touchmove', throttle((e) => {
      // 滑动逻辑(如更新位置)
      updatePosition(e.touches[0].clientX);
    }));
    
  • 使用touch-action属性:告诉浏览器无需处理某些触摸行为(如缩放、滑动),减少浏览器的默认触摸处理开销;

    .swipe-container {
      touch-action: pan-y; /* 仅允许垂直滑动,禁用其他触摸行为(如双击缩放) */
    }
    .button {
      touch-action: manipulation; /* 优化点击,禁用双击缩放,减少300ms延迟 */
    }
    

3. 优化滚动性能

移动端滚动卡顿是常见问题,主要因滚动时触发频繁重排/重绘或主线程阻塞。

优化方案

  • 使用原生滚动:避免用JS模拟滚动(如transform: translateY配合touchmove),原生滚动由浏览器 compositor 线程处理,更流畅;

  • 启用硬件加速:对滚动容器使用will-change: scroll-position(谨慎使用,避免滥用导致内存占用过高);

  • 避免滚动时的重排:滚动过程中不修改会触发重排的属性(如offsetTopwidth),如需读取布局信息,提前缓存;

  • 使用passive: true优化滚动事件:避免滚动事件监听阻塞浏览器滚动(防止“滚动卡住”);

    // 优化滚动事件:passive为true表示不会调用preventDefault(),浏览器可优化滚动
    document.addEventListener('scroll', handleScroll, { passive: true });
    

三、动画与渲染优化:避免掉帧

移动端GPU/CPU性能有限,过度或低效的动画会导致掉帧(低于60fps),表现为动画卡顿、不连贯。

1. 优先使用CSS动画,避免JS动画

  • CSS动画由浏览器 compositor 线程(独立于主线程)处理,性能优于JS动画(JS动画需主线程计算,易被阻塞);

  • 仅用transformopacity做动画:这两个属性的变化仅触发“合成”(composite),不触发重排(reflow)或重绘(repaint),是性能最优的动画属性;

    /* 高效动画:仅用transform和opacity */
    .box {
      transition: transform 0.3s, opacity 0.3s;
    }
    .box:hover {
      transform: translateX(10px); /* 仅触发合成 */
      opacity: 0.8; /* 仅触发合成 */
    }
    
    /* 低效动画:会触发重排(避免) */
    .bad-box {
      transition: left 0.3s; /* left变化会触发重排 */
    }
    

2. 限制动画数量和复杂度

  • 同一时间运行的动画不超过3个:过多动画会耗尽GPU资源;
  • 避免复杂动画效果:如模糊(filter: blur())、阴影动画(box-shadow),这些效果计算成本高,可改用静态阴影+transform模拟;
  • 动画结束后及时清除:用animation-fill-mode: forwards保持最终状态,避免动画无限循环(尤其在后台运行时浪费资源)。

3. 谨慎使用will-change

will-change告诉浏览器“元素即将变化”,让浏览器提前准备优化,但滥用会导致GPU内存占用过高(尤其移动端)。

正确用法:仅对“即将动画的元素”临时设置,动画结束后移除;

/* 错误:全局设置,浪费资源 */
/* * { will-change: transform; } */

/* 正确:仅在需要时设置 */
.box:hover {
  will-change: transform; /* 鼠标悬停时提示浏览器即将动画 */
  transform: scale(1.1);
}
.box {
  transition: transform 0.3s;
}

四、资源加载优化:应对弱网环境

移动端网络环境复杂(3G/4G/5G切换、信号不稳定),需通过资源优化减少加载时间和流量消耗。

1. 减少资源体积

  • 图片压缩与格式优化
    • 优先使用WebP/AVIF格式(比JPG小30%+),配合picture标签降级兼容旧浏览器;
    • 对非重要图片(如背景图)使用低分辨率(如72dpi),避免“高清图浪费流量”;
  • JS/CSS压缩与分割
    • 生产环境启用代码压缩(Terser压缩JS,CSSNano压缩CSS);
    • 按路由/组件分割代码(如Vue的() => import()、React的React.lazy),仅加载当前页面必需代码;
  • 移除冗余资源:通过Tree Shaking删除未使用代码,避免引入不必要的第三方库(如用轻量库dayjs替代moment.js)。

2. 懒加载非首屏资源

  • 图片懒加载:仅当图片进入可视区域时加载,减少初始请求数(用原生loading="lazy"或IntersectionObserver实现);

    <!-- 原生懒加载(现代浏览器支持) -->
    <img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="懒加载图片">
    
  • 组件懒加载:首屏外的组件(如弹窗、底部评论)延迟加载,避免初始渲染压力。

3. 预加载关键资源

对首屏必需的资源(如核心JS、首屏CSS、Logo)使用preload强制优先加载;对跨域资源(如CDN)使用preconnect提前建立连接,减少请求延迟(详见“预加载与预连接”章节)。

五、内存与DOM优化:避免崩溃与卡顿

移动端设备内存有限,过多DOM节点或内存泄漏会导致页面崩溃、滑动卡顿。

1. 减少DOM节点数量

  • 避免嵌套过深(建议DOM层级≤6层):深层级DOM会增加浏览器布局计算时间;
  • 移除冗余节点:如空<div>、重复的容器标签,用CSS伪元素(::before/::after)替代简单装饰性节点;
  • 长列表用虚拟列表:对1000+条的列表(如聊天记录、商品列表),使用虚拟列表(如vue-virtual-scrollerreact-window)仅渲染可视区域项,减少DOM节点。

2. 避免内存泄漏

移动端内存不足时,浏览器会强制关闭页面,需特别注意:

  • 及时移除事件监听:尤其是scrolltouchmove等高频事件,在组件卸载时解绑;

    // Vue组件示例:卸载时移除事件监听
    onMounted(() => {
      window.addEventListener('scroll', handleScroll);
    });
    onUnmounted(() => {
      window.removeEventListener('scroll', handleScroll); // 关键:及时移除
    });
    
  • 清理定时器:setInterval/setTimeout未及时清除会导致内存泄漏,组件卸载时用clearInterval/clearTimeout销毁;

  • 避免大量图片预加载:预加载图片会占用内存,按需加载(如滑动到附近再加载)。

总结

移动端前端性能优化需围绕“适配精准、交互流畅、加载快速、资源可控”四大目标,核心策略包括:

  • 适配:合理设置viewport、用flex/rem实现弹性布局;
  • 交互:消除点击延迟、节流触摸事件、优化滚动;
  • 动画:用CSS动画+transform/opacity,限制动画数量;
  • 加载:压缩资源、懒加载、预加载关键资源;
  • 内存:减少DOM节点、避免内存泄漏。

需结合业务场景(如电商、资讯、工具类)优先优化用户感知最强的环节(如首屏加载、滑动流畅度),通过Chrome DevTools的Performance面板模拟移动端环境(如3G网络、CPU降速)定位瓶颈。

27.什么是内存泄漏?如何检测和解决前端内存泄漏(如使用 Chrome DevTools、避免全局变量)?

一、内存泄漏(Memory Leak)的定义

内存泄漏指程序中已不再使用的内存未能被浏览器的垃圾回收机制(Garbage Collection, GC)正确释放,导致内存占用持续增长的现象。在前端中,内存泄漏会导致页面逐渐卡顿、响应变慢,严重时会触发浏览器崩溃(尤其是移动端设备,内存资源有限)。

二、前端常见的内存泄漏场景

前端内存泄漏的核心原因是“无用对象被意外保留了引用,导致垃圾回收机制无法回收”。常见场景包括:

1. 意外创建的全局变量

全局变量在页面生命周期内不会被自动回收(除非主动删除),若无意中将大量数据存储在全局变量中,会导致内存持续占用。

示例

// 错误:未声明变量,默认挂载到window(全局变量)
function handleData() {
  // 忘记用let/const声明,data成为window的属性
  data = Array(10000).fill('large data'); // 大数组无法被回收
}
handleData();

2. 未移除的事件监听器

事件监听器(如scrollresizeclick)会建立DOM与回调函数的引用关系。若DOM被移除但监听器未解绑,或组件卸载后监听器仍存在,会导致关联的函数和数据无法回收。

示例

// 错误:组件卸载后,scroll监听器未移除
class MyComponent {
  constructor() {
    window.addEventListener('scroll', this.handleScroll); // 绑定全局事件
  }
  handleScroll() { /* 处理滚动逻辑 */ }
  destroy() {
    // 忘记移除监听器,this.handleScroll及关联数据无法回收
    // window.removeEventListener('scroll', this.handleScroll); 
  }
}
const comp = new MyComponent();
comp.destroy(); // 组件销毁后,监听器仍存在

3. 未清除的定时器/计时器

setIntervalsetTimeout若未及时清除,会持续引用回调函数及内部数据,即使页面已不需要这些逻辑,相关内存也无法释放。

示例

// 错误:定时器未清除,导致回调及data持续占用内存
function startTimer() {
  const data = Array(10000).fill('timer data');
  setInterval(() => {
    console.log(data); // 定时器引用data
  }, 1000);
}
startTimer();
// 后续未调用clearInterval,定时器永久运行,data无法回收

4. 闭包导致的变量滞留

闭包会保留对外部作用域变量的引用,若闭包本身被长期持有(如全局变量引用闭包),会导致外部变量无法回收。

示例

// 错误:闭包被全局变量引用,导致largeData无法回收
let globalFn;
function outer() {
  const largeData = Array(10000).fill('closure data'); // 大数组
  globalFn = function inner() { 
    // 闭包引用largeData
    console.log(largeData.length); 
  };
}
outer();
// 即使outer执行完毕,globalFn仍引用inner,inner引用largeData,导致内存泄漏

5. DOM节点引用未清理

若JS中保留了已从DOM树中移除的节点引用(如存在数组、对象中),垃圾回收机制会认为该节点仍在使用,无法回收其内存。

示例

// 错误:DOM节点已移除,但JS中仍有引用
const list = [];
function removeNode() {
  const node = document.getElementById('target');
  list.push(node); // 节点被list引用
  node.parentNode.removeChild(node); // 从DOM中移除节点
  // 此时node仍在list中,无法被回收
}
removeNode();

三、内存泄漏的检测工具与方法(Chrome DevTools)

Chrome浏览器的开发者工具提供了专门的内存分析功能,可定位泄漏对象和原因。核心工具是Memory面板Performance面板

1. 初步判断:观察内存趋势(Performance面板)

通过记录页面操作过程中的内存变化,判断是否存在泄漏(内存只增不减)。

操作步骤

  1. 打开Chrome DevTools → 切换到Performance面板;
  2. 勾选Memory选项(记录内存数据);
  3. 点击录制按钮(●),然后在页面上执行可能导致泄漏的操作(如反复切换组件、触发事件);
  4. 点击停止录制(■),查看内存曲线:
    • 若内存曲线持续上升且操作结束后不下降,说明可能存在泄漏。

2. 定位泄漏对象:Heap Snapshot(堆快照)

堆快照可捕获当前内存中的所有对象,通过对比多次快照,找出“持续存在且不应存在的对象”。

操作步骤

  1. 打开DevTools → 切换到Memory面板;
  2. 选择Heap Snapshot,点击Take snapshot生成初始快照;
  3. 执行可能导致泄漏的操作(如打开再关闭组件);
  4. 生成第二次快照,在快照列表中选择Compare(对比模式);
  5. 筛选Retained Size(保留大小,即该对象占用且无法被回收的内存)增长的对象,查看其Retainers(引用链),定位是谁在引用这些对象(如全局变量、事件监听器)。

3. 跟踪内存分配:Allocation Sampling

实时跟踪内存分配情况,定位哪些函数创建了大量未回收的对象。

操作步骤

  1. Memory面板中选择Allocation Sampling
  2. 点击Start,执行页面操作;
  3. 操作结束后点击Stop,查看“Allocation stack”(分配栈),找出分配了大量内存且未释放的函数。

四、内存泄漏的解决策略

针对上述场景,解决核心是“及时释放不再使用的资源,切断无用对象的引用链”

1. 避免意外全局变量

  • 启用严格模式('use strict'),禁止未声明的变量自动成为全局变量;

    'use strict'; // 严格模式下,未声明的变量会报错
    function handleData() {
      const data = Array(10000).fill('data'); // 用let/const声明局部变量
    }
    
  • 若需使用全局变量,在不需要时手动删除(delete window.globalVar)。

2. 及时移除事件监听器

  • 组件卸载或DOM移除前,用removeEventListener解绑事件;

    class MyComponent {
      constructor() {
        this.handleScroll = this.handleScroll.bind(this);
        window.addEventListener('scroll', this.handleScroll);
      }
      handleScroll() { /* 逻辑 */ }
      destroy() {
        // 关键:移除监听器
        window.removeEventListener('scroll', this.handleScroll);
      }
    }
    
  • 框架中使用生命周期钩子(如Vue的onUnmounted、React的useEffect清理函数):

    // React示例:useEffect清理函数移除监听器
    useEffect(() => {
      const handleScroll = () => { /* 逻辑 */ };
      window.addEventListener('scroll', handleScroll);
      // 组件卸载时执行清理
      return () => window.removeEventListener('scroll', handleScroll);
    }, []);
    

3. 清除定时器/计时器

  • clearInterval/clearTimeout及时销毁不再需要的定时器;

    function startTimer() {
      const data = Array(10000).fill('data');
      const timer = setInterval(() => {
        console.log(data);
      }, 1000);
      // 提供清除方法
      return () => clearInterval(timer);
    }
    const clearTimer = startTimer();
    // 不需要时清除
    clearTimer();
    

4. 控制闭包引用

  • 避免闭包保留不必要的变量(只引用必需的数据);

  • 若闭包被全局引用,在不需要时将全局变量设为null(切断引用链):

    let globalFn = null; // 初始化为null
    function outer() {
      const largeData = Array(10000).fill('data');
      globalFn = function inner() {
        console.log(largeData.length);
      };
    }
    outer();
    // 不需要时,切断引用
    globalFn = null; // inner及largeData可被回收
    

5. 清理DOM引用

  • 从DOM移除节点后,同步删除JS中对该节点的引用(如数组、对象中的存储);

    const list = [];
    function removeNode() {
      const node = document.getElementById('target');
      // 移除DOM节点
      node.parentNode.removeChild(node);
      // 清除JS引用
      const index = list.indexOf(node);
      if (index !== -1) list.splice(index, 1);
    }
    

总结

内存泄漏的本质是“无用对象被意外引用”,检测依赖Chrome DevTools的Heap Snapshot和Performance面板,解决核心是“及时释放资源,切断引用链”。日常开发中,应重点关注事件监听器、定时器、全局变量和闭包的使用,结合框架生命周期钩子(如组件卸载时的清理逻辑)预防泄漏,尤其在复杂应用(如SPA)中需定期进行内存检测。

28.如何优化第三方库的加载?(如按需引入、CDN 加载、替换轻量级库)

第三方库(如UI组件库、工具库、图表库等)是前端面试的重要依赖,但它们往往体积庞大(动辄数百KB甚至几MB),若加载策略不当,会显著增加页面加载时间和内存占用。优化第三方库的核心思路是:“减少加载体积”“优化加载时机”“提升加载效率”,具体策略如下:

一、按需引入:只加载用到的功能

大多数第三方库(如工具库、UI组件库)提供了模块化设计,支持“按需引入”——仅加载项目中实际使用的功能,而非全量引入,从而大幅减少加载体积。

1. 工具库的按需引入(如Lodash、date-fns)

全量引入工具库会包含大量未使用的API,按需引入可减少90%以上的体积。

反例(全量引入,体积大)

// 引入整个Lodash(体积≈70KB,gzip后)
import _ from 'lodash'; 
_.debounce(/* ... */);
_.cloneDeep(/* ... */);

正例(按需引入)

// 仅引入需要的API(体积≈5KB,gzip后)
import debounce from 'lodash/debounce'; 
import cloneDeep from 'lodash/cloneDeep';
debounce(/* ... */);
cloneDeep(/* ... */);

进阶:使用babel-plugin-lodash等插件自动按需引入,无需手动调整代码:

// 配置babel-plugin-lodash后,可直接写全量引入的语法,插件会自动转换为按需引入
import _ from 'lodash'; 
_.debounce(/* ... */); // 实际被编译为引入lodash/debounce

2. UI组件库的按需引入(如Element Plus、Ant Design)

UI组件库通常包含数十个组件,全量引入会导致体积激增(如Element Plus全量引入≈500KB+),按需引入仅加载使用的组件。

以Element Plus为例

  • 安装按需引入插件:npm install unplugin-vue-components unplugin-auto-import -D

  • 配置Vite/Webpack,自动检测并引入使用的组件及样式:

    // vite.config.js
    import Components from 'unplugin-vue-components/vite';
    import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
    
    export default {
      plugins: [
        Components({
          resolvers: [ElementPlusResolver()], // 自动引入Element Plus组件
        }),
      ],
    };
    
  • 直接使用组件,无需手动import:

    <template>
      <el-button>按钮</el-button> <!-- 仅引入Button组件及相关样式 -->
    </template>
    

二、CDN加载:利用分布式网络加速

第三方库通常是多个项目的公共依赖(如React、Vue、jQuery),使用CDN(内容分发网络)加载可带来两大优势:

  1. 就近加载:CDN通过全球分布式节点,让用户从最近的服务器获取资源,减少网络延迟;
  2. 缓存复用:若用户访问过其他使用相同CDN库的网站,库文件可能已被浏览器缓存,无需重新下载。

1. 选择合适的CDN与版本

  • 公共CDN推荐:unpkg、jsDelivr、CDNJS(支持大多数主流库,稳定性高);
  • 版本锁定:避免使用latest标签(可能因版本更新导致兼容性问题),锁定具体版本(如vue@3.3.4);
  • HTTPS协议:确保CDN资源使用HTTPS,避免混合内容警告(HTTP+HTTPS)。

2. 实现方式:通过<script>标签引入

在HTML中直接通过CDN引入第三方库,同时配置webpack externals(或Vite externals)排除打包,避免库被打包到项目bundle中。

示例(Vue项目用CDN加载Vue)

  1. HTML中引入CDN资源:

    <!-- index.html -->
    <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
    
  2. 配置Vite排除Vue打包:

    // vite.config.js
    export default {
      build: {
        rollupOptions: {
          externals: {
            vue: 'Vue' // 告诉打包工具:全局变量Vue对应vue模块
          }
        }
      }
    };
    
  3. 项目中正常使用:

    import { createApp } from 'vue'; // 实际使用CDN提供的全局Vue变量
    createApp(App).mount('#app');
    

3. 增强CDN可靠性(可选)

  • Fallback机制:若CDN加载失败,自动切换到本地备份资源:

    <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
    <script>
      // 检查Vue是否加载成功,失败则加载本地备份
      if (typeof Vue === 'undefined') {
        document.write('<script src="/libs/vue@3.3.4.js"><\/script>');
      }
    </script>
    
  • 预连接CDN域名:用<link rel="preconnect">提前与CDN建立连接,减少加载延迟:

    <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
    

三、替换为轻量级库:用“小而美”替代“大而全”

许多场景下,我们使用的第三方库功能过剩(如仅用日期格式化却引入完整的moment.js),此时可替换为功能匹配的轻量级库,直接减少资源体积。

常见替换方案(体积对比,gzip后)

场景 传统库(体积) 轻量级替代库(体积) 体积减少比例
日期处理 Moment.js(24KB) Day.js(2KB)、date-fns(5KB) 80%+
DOM操作 jQuery(30KB) Zepto(9KB)、cash-dom(5KB) 70%+
图表展示 ECharts(200KB+) Chart.js(30KB)、Frappe Charts(15KB) 80%+
状态管理 Redux(16KB)+ 中间件 Zustand(3KB)、Jotai(2KB) 80%+
工具函数 Lodash(70KB) Lodash-es(按需引入)、radash(10KB) 70%+

替换注意事项

  • API兼容性:轻量级库可能与传统库API不同(如Day.js大部分兼容Moment.js,但非全部),需评估迁移成本;
  • 功能完整性:确保轻量级库满足业务需求(如Chart.js不支持ECharts的复杂地图,但基础图表足够);
  • 社区活跃度:优先选择维护活跃的库(如Day.js持续更新,而某些小众库可能停止维护)。

四、动态加载:非首屏库延迟加载

对于非首屏必需的第三方库(如富文本编辑器、数据可视化图表、地图SDK),可采用“动态加载”——在用户需要时(如点击“编辑”按钮、进入图表页)再加载,避免阻塞首屏渲染。

实现方式:动态import()

利用ES6动态import()(返回Promise),在需要时异步加载库,配合await.then()使用。

示例(动态加载富文本编辑器TinyMCE)

// 点击“编辑”按钮时才加载TinyMCE
document.getElementById('editBtn').addEventListener('click', async () => {
  // 显示加载状态
  const loading = document.getElementById('loading');
  loading.style.display = 'block';

  try {
    // 动态加载TinyMCE(从CDN或本地)
    const { default: tinymce } = await import('tinymce/tinymce.min.js');
    // 加载完成后初始化
    tinymce.init({ selector: '#editor' });
  } finally {
    // 隐藏加载状态
    loading.style.display = 'none';
  }
});

进阶:预加载非首屏库(prefetch

对于“用户可能很快用到”的库(如从列表页进入详情页可能需要的图表库),可通过<link rel="prefetch">在浏览器空闲时低优先级加载,提前缓存:

<!-- 浏览器空闲时预加载图表库,为后续操作做准备 -->
<link rel="prefetch" href="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js">

五、其他优化技巧

  1. 合并重复库:若项目中存在多个功能相似的库(如同时引入lodashunderscore),统一替换为一个,减少冗余;
  2. 使用生产环境版本:第三方库通常提供开发版(含调试信息)和生产版(压缩、去注释),生产环境务必使用生产版(如vue.global.prod.js而非vue.global.js);
  3. 监控第三方库性能:用Lighthouse或Web Vitals检测第三方库的加载耗时,优先优化对首屏影响最大的库(如阻塞渲染的JS/CSS)。

总结

第三方库优化的核心是“按需加载、高效传输、精简体积”:

  • 短期见效:优先使用按需引入(减少体积)和CDN加载(加速传输);
  • 长期优化:用轻量级库替换(从源头减少体积),对非首屏库采用动态加载(不阻塞首屏)。

需结合项目依赖的具体库和业务场景选择策略,避免为了优化而过度增加开发复杂度(如小众库的迁移成本可能高于收益)。

29.简述前端性能监控的方式(如埋点监控、性能 API、第三方工具)

前端性能监控是保障用户体验的关键环节,通过收集、分析页面加载、交互、资源等维度的性能数据,定位性能瓶颈并优化。主要监控方式可分为三大类:埋点监控、浏览器性能API、第三方工具,具体如下:

一、埋点监控(自定义数据收集)

通过在代码中手动或自动插入“埋点”,收集关键节点的性能数据(如加载时间、交互延迟),并上报到服务器进行分析。

1. 手动埋点

开发者在关键业务场景(如页面初始化、按钮点击、接口请求)主动插入代码,记录时间戳或性能指标。

  • 核心思路:用 Date.now()performance.now() 记录事件开始/结束时间,计算耗时并上报。

  • 示例

    // 监控接口请求耗时
    function requestData(url) {
      const start = performance.now(); // 更精确的时间(微秒级)
      fetch(url)
        .then(res => res.json())
        .then(data => {
          const end = performance.now();
          const duration = end - start; // 接口耗时
          // 上报数据(用sendBeacon避免阻塞主线程)
          navigator.sendBeacon('/monitor', JSON.stringify({
            type: 'api',
            url,
            duration,
            timestamp: Date.now()
          }));
        });
    }
    
    // 监控首屏渲染完成时间
    window.addEventListener('load', () => {
      const firstScreenTime = performance.now();
      // 上报首屏时间
    });
    
  • 适用场景:业务相关的定制化指标(如“加入购物车”按钮的响应时间)、小型项目快速接入。

2. 自动埋点

通过框架插件或工具自动捕获通用性能事件(如路由切换、资源加载、DOM操作),减少手动代码侵入。

  • 实现方式
    • 基于框架钩子(如Vue的router.afterEach、React的useEffect)监听页面切换;
    • 重写原生API(如fetchXMLHttpRequest)自动记录接口耗时;
    • 监听DOMContentLoadedload等事件捕获页面加载阶段。
  • 工具示例:自研埋点SDK(如封装monitor.js)、百度统计的“热力图+性能监控”模块。

二、浏览器性能API(原生数据采集)

现代浏览器提供了一系列原生API,可直接获取页面加载、资源、交互等底层性能数据,是性能监控的“数据源头”。

1. Performance 对象

核心API,提供页面生命周期各阶段的时间戳、资源加载详情等数据。

  • **performance.timing**:记录页面从导航到加载完成的关键时间节点(如DNS解析、TCP连接、DOM渲染)。
    示例(计算首屏加载时间):

    const timing = performance.timing;
    // DNS解析耗时
    const dnsTime = timing.domainLookupEnd - timing.domainLookupStart;
    // 首屏加载完成时间(从导航到load事件触发)
    const loadTime = timing.loadEventEnd - timing.navigationStart;
    
  • **performance.getEntriesByType()**:获取各类资源(JS、CSS、图片、接口)的加载详情(如耗时、大小、协议)。
    示例(监控所有JS资源加载耗时):

    const jsResources = performance.getEntriesByType('script');
    jsResources.forEach(resource => {
      console.log('JS资源:', resource.name, '耗时:', resource.duration);
    });
    

2. 核心Web指标(Core Web Vitals)API

浏览器原生支持监控用户体验相关的核心指标(Google推荐),包括:

  • LCP(最大内容绘制):衡量加载性能(首屏最大内容渲染时间,目标<2.5s);

  • FID(首次输入延迟):衡量交互响应性(首次用户输入到反馈的延迟,目标<100ms);

  • CLS(累积布局偏移):衡量视觉稳定性(页面元素意外偏移的总和,目标<0.1)。

  • 监控方式:用 PerformanceObserver 监听这些指标的变化:

    // 监控LCP
    new PerformanceObserver((entries) => {
      entries.forEach(entry => {
        const lcpTime = entry.startTime; // LCP发生时间
        // 上报LCP数据
      });
    }).observe({ type: 'largest-contentful-paint', buffered: true });
    

三、第三方工具(一站式监控方案)

第三方工具封装了上述埋点和API能力,提供数据采集、可视化分析、告警等全流程功能,适合中大型项目。

1. 开源工具(自建监控系统)

  • Lighthouse:Chrome官方工具,通过命令行或DevTools生成性能报告(含加载、交互、SEO等指标),适合本地测试和CI集成;
  • Web Vitals Extension:Chrome插件,实时显示当前页面的Core Web Vitals指标,辅助开发调试。

2. 商业APM工具(全链路监控)

  • New Relic/Datadog:支持前端+后端全链路性能监控,可关联接口耗时、服务器响应时间,定位前后端性能瓶颈;
  • Sentry:以错误监控为核心,同时集成性能监控(如页面加载时间、API耗时),支持异常与性能数据联动分析;
  • 国内工具:阿里云ARMS、腾讯前端性能监控,提供本地化部署、国内CDN加速,适配国内网络环境。

3. 特点与优势

  • 无需手动开发数据存储、分析、可视化模块;
  • 支持大规模数据采集(百万级用户)和实时告警(如性能指标超过阈值时触发邮件/短信通知);
  • 提供行业基准对比(如“你的页面LCP优于70%同类网站”)。

总结

前端性能监控的方式各有侧重:

  • 埋点监控:灵活定制,适合业务相关指标;
  • 性能API:原生数据源头,适合深度定制化监控;
  • 第三方工具:开箱即用,适合全链路、大规模监控。

实际应用中,通常结合使用(如用Performance API采集数据,通过埋点上报到第三方工具),核心目标是覆盖“加载速度、交互响应、视觉稳定性”三大维度,持续优化用户体验。

30.常用的前端性能分析工具有哪些(如 Chrome DevTools、Lighthouse、WebPageTest)?如何使用?

前端性能分析工具是定位性能瓶颈的核心手段,不同工具侧重不同场景(如开发调试、生成报告、跨地区测试)。以下是最常用的工具及具体使用方法:

一、Chrome DevTools(开发调试必备)

Chrome浏览器内置的开发者工具,提供实时性能监控、网络分析、代码调试等功能,适合开发阶段定位具体性能问题。

核心面板及使用

1. Performance 面板(分析运行时性能)

作用:记录并分析页面运行时的性能数据(如帧率、主线程任务、渲染耗时),定位卡顿、长任务等问题。

使用步骤

  1. 打开Chrome,访问目标页面,按 F12Ctrl+Shift+I 打开DevTools;
  2. 切换到 Performance 面板;
  3. 点击左上角的 录制按钮(●),然后在页面上执行操作(如滚动、点击按钮);
  4. 操作完成后点击 停止按钮(■),生成性能报告:
    • 概览区:查看FPS(帧率,低于60表示卡顿)、CPU使用率(过高说明主线程繁忙)、网络请求;
    • 主线程时间线:分析任务耗时,红色长条表示“长任务”(>50ms,会阻塞交互);
    • 调用栈:点击长任务,查看具体函数调用,定位耗时操作(如复杂DOM操作、大量计算)。

示例场景:页面滚动卡顿,通过Performance发现某scroll事件回调函数执行耗时200ms,需优化该函数(如节流、简化计算)。

2. Network 面板(分析资源加载性能)

作用:监控所有资源(JS、CSS、图片、接口)的加载时间、大小、顺序,定位加载瓶颈(如大资源、慢接口、请求瀑布流不合理)。

使用步骤

  1. 打开DevTools → Network 面板;
  2. (可选)点击 No throttling 选择网络节流(如Fast 3G),模拟真实网络环境;
  3. 刷新页面,查看资源加载瀑布流:
    • 列含义Name(资源名)、Size(资源大小)、Time(加载总耗时)、Waterfall(加载阶段细分,如DNS、TCP、下载);
    • 关键操作
      • 筛选资源类型(如点击JS只看JS文件);
      • 查看慢资源(耗时过长的资源,如大图片、未压缩的JS);
      • 分析请求依赖(如CSS阻塞JS执行,或JS加载顺序不合理)。

示例场景:首屏加载慢,Network显示某张未压缩的图片(2MB)加载耗时3s,需优化为WebP格式并压缩至200KB。

3. Lighthouse 面板(生成综合性能报告)

作用:自动化分析页面性能、可访问性、SEO等指标,生成评分和优化建议(基于Core Web Vitals等标准)。

使用步骤

  1. 打开DevTools → Lighthouse 面板;
  2. 勾选要测试的类别(至少勾选Performance);
  3. 选择设备(MobileDesktop),点击 Analyze page load
  4. 等待测试完成,查看报告:
    • 性能评分:0-100分,基于加载速度、交互响应等指标;
    • 关键指标:LCP(最大内容绘制)、FID(首次输入延迟)、CLS(累积布局偏移)等;
    • 优化建议:如“启用文本压缩”“预加载关键请求”,点击可查看具体解决方法。

二、Lighthouse(独立工具,生成标准化报告)

Lighthouse除了在Chrome DevTools中使用,还可通过命令行Web版运行,适合CI/CD集成或批量测试。

命令行使用(适合开发者)

  1. 安装Node.js(需v14+);
  2. 全局安装Lighthouse:npm install -g lighthouse
  3. 运行测试:lighthouse https://example.com --view--view自动打开报告);
  4. 报告生成在当前目录(HTML格式),内容与DevTools面板一致,可保存或分享。

Web版(无需安装,快速测试)

访问 PageSpeed Insights(Google提供,基于Lighthouse),输入URL即可生成性能报告,适合快速获取评分和建议。

三、WebPageTest(跨地区、深度网络分析)

特点:从全球多个测试节点(如北京、纽约、伦敦)模拟真实用户环境,提供更详细的网络层分析(如CDN性能、TCP握手耗时),适合分析不同地区的加载差异。

使用步骤

  1. 访问 WebPageTest
  2. 输入测试URL(如https://example.com);
  3. 选择测试地点(如Beijing, China)、浏览器(如Chrome)、设备(如Mobile);
  4. 点击 Start Test,等待测试完成(约1-2分钟);
  5. 查看核心结果:
    • Summary:关键指标(First Contentful Paint、Time to Interactive等)和评分;
    • Waterfall:比Chrome Network更详细的加载阶段分析(含DNS、TCP、TLS各阶段耗时);
    • Filmstrip:页面加载过程的帧截图,直观看到渲染进度;
    • Compare:可对比不同测试地点或优化前后的性能差异。

示例场景:国内用户反馈页面慢,WebPageTest显示北京节点的DNS解析耗时800ms,需更换国内DNS服务商。

四、其他实用工具

  1. Sentry:实时监控生产环境性能,关联错误与性能数据(如某接口超时导致页面卡顿),支持告警;
  2. Chrome Performance Monitor:DevTools中的小工具(More tools → Performance monitor),实时显示CPU、内存、DOM节点数等指标变化;
  3. Web Vitals Extension:Chrome插件,一键查看当前页面的Core Web Vitals指标(LCP、FID、CLS),适合快速验证优化效果。

总结

  • 开发调试:优先用Chrome DevTools的Performance(运行时)和Network(资源加载)面板;
  • 生成报告:用Lighthouse(DevTools/命令行)或PageSpeed Insights,获取标准化评分和建议;
  • 跨地区/深度分析:用WebPageTest,定位网络层或地区性性能问题。

实际使用中,通常结合多种工具:先用Lighthouse生成整体报告,再用Chrome DevTools深入分析具体瓶颈,最后用WebPageTest验证优化效果在不同地区的表现。