普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月13日首页

TodoList应用:SPA应用首屏性能优化实践

2026年3月13日 18:19

hello,大家好,我是唐叔,今天想介绍的是 SPA - 单页应用的首屏性能问题优化的一些实践,也是我理解的 SPA 应用最大的技术难点吧。

SPA 的优势很明显:一次加载,无缝交互,适合多端复用。但代价也很大——首屏需要加载所有资源,一旦代码庞大,白屏时间就会变长。这就是我们常说的“首屏性能问题”。

下面就以唐叔最近一直在开发的 TodoList 应用为例,分享几种优化手段。

优化方式 1:骨架屏

严格说,骨架屏并不是优化性能,而是优化用户体验。

在数据加载完成前,先展示一个灰色的“页面轮廓”,避免用户面对白屏。

第一步,前端 html 页面,在 HTML 中预留骨架屏结构,具体可以基于你原本页面的框架去处理:

第二步,在编写 JS 初始化操作时,先显示骨架屏,然后加载数据,加载完数据再隐藏骨架屏。

大体编码是这样的:

class App {
   // 初始化应用
    async init() {
        try {
          // 显示加载状态
 Utils.setLoading(true);
            // 加载数据
        } catch (error) {
            // 异常报错
        } finally {
            // 隐藏加载状态
            Utils.setLoading(false);

            // 隐藏骨架屏
            const skeletonScreen = document.getElementById('skeleton-screen');
            if (skeletonScreen) {
                skeletonScreen.style.display = 'none';
            }
        }
    }
}

document.addEventListener('DOMContentLoaded', () => {
    // 显示骨架屏
    const skeletonScreen = document.getElementById('skeleton-screen');
    if (skeletonScreen) {
        skeletonScreen.style.display = 'flex';
    }

    // 延迟初始化,确保所有资源加载完成
    setTimeout(() => {
        app.init();
    }, 100);
});

额外建议:现在的开发更多是“思路驱动实现”,你可以用 AI 快速生成骨架屏代码,前提是你清楚它的原理。

优化方式 2:前端缓存

缓存虽然对首次加载无效,但对二次打开提升巨大。

常见前端存储方式主要有下述几类:

存储方式 容量 持久性 异步/同步 数据结构 同源限制
Cookie ~4KB 可设置 同步 字符串
localStorage 5-10MB 永久 同步 字符串
sessionStorage 5-10MB 会话级 同步 字符串
IndexedDB >250MB 永久 异步 结构化
Cache API 不定 永久 异步 Request/Response
FileSystem 不定 永久 异步 文件

TodoList 中,我们使用 localStorage 存储用户偏好(如主题、语言)。如果你开发的是轻量化应用,完全可以用 IndexedDB 作为整个应用的数据存储层。

💡 不过要补充说明的是,上述各类存储方式,可能存在浏览器兼容性问题。像 TodoList 用的后端是 pywebview,开启 localStorage 通过配置 private_mode

优化方式 3:路由懒加载

SPA 首屏并不需要加载所有页面模块时,可以把部分组件延迟到用户访问时再加载。

TodoList 移动端中,我正在做将「左侧抽屉弹窗」改为懒加载,等用户点击时再加载对应数据,减少首屏负担。

优化方式 N:其他策略

上述几种方式,是目前 TodoList 应用主用的优化方式,当然还有其他的方式,这里以我了解到的做展开介绍,当前不是专业前端可能了解的不多,其他同学知道的其他方式也可以在评论区补充说明。

  • 静态资源懒加载

    和路由懒加载类似,类似图片等高占用体积的资源,可以考虑使用这种方式,不过像 TodoList 应用,前端和后端都是写在一个包里面的,没有纯粹的后端服务,貌似用不了。

    <img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />
    
  • Gzip 压缩

    现代构建工具(如 Webpack、Vite)会自动生成压缩后的代码包,服务端开启 Gzip 后即可生效,显著减少传输体积。不过 TodoList 是纯 HTML/CSS/JS 页面,估计还真的不好用这种方式。


以上是 TodoList 项目中正在实践的首屏优化策略。作为本专栏的开篇,希望能帮你少踩一些坑,也欢迎大家在评论区补充你常用的优化方式。

昨天以前首页

iOS NotificationCenter Observer 的隐性性能代价

作者 visual_zhang
2026年3月12日 00:17

引言

iOS 9 之后,Apple 为 NSNotificationCenter 的 target-action 模式引入了 zeroing weak reference。当 observer 对象被释放后,系统自动将内部的 weak reference 置为 nil,不再向其投递通知,也不会产生野指针 crash。

Apple 文档对此也有明确说明:

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.

这条规则被广泛接受,许多团队因此在代码规范中不再严格要求管理 observer 生命周期。然而,"不会 crash"并不等于"没有性能影响"。本文通过一个真实的线上卡死案例,探讨 NotificationCenter observer 管理不当可能带来的隐性性能问题。

一个线上卡死案例

在一次线上卡死监控中,我们发现一类卡死的比例明显上升。主线程被卡住 13 秒,CPU 占用 98.8%,处于 running 状态(不是锁等待)。堆栈顶部如下:

_weak_unregister_no_lock
_objc_moveWeak
__CFXNotificationRegistrarAddObserver
SomeTimeViewModel.componentMount()     ← 调用 NotificationCenter.default.addObserver(...)

卡死发生在一个 ViewModel 的初始化阶段——调用 addObserver(self, selector:, name:, object:) 注册通知时。注册一个通知 observer 本身应该是一个非常轻量的操作,为什么会导致 13 秒的卡死?

经过排查,这个问题的根因并不是"忘记 removeObserver 导致 dead entries 累积"(事实上代码在 dealloc 链路中已经正确调用了 removeObserver),而是一个更容易被忽视的问题:同一个通知名下积累了过多的 live observers

NotificationCenter 的内部机制

要理解这个问题,需要了解 NSNotificationCenter 在 iOS 9+ 中的内部机制。

注册表结构

当调用 addObserver:selector:name:object: 时,NotificationCenter 在内部维护一个注册表(registrar),按通知名称索引,存储所有注册信息。每条注册信息大致包括:

  • 一个指向 observer 的 weak reference
  • selector
  • notification name
  • object filter

这些信息存储在 CoreFoundation 内部的数据结构(类似于哈希表 + 数组)中。

扩容与 weak reference 迁移

和 HashMap 类似,NotificationCenter 的内部存储在容量不足时会扩容——分配更大的存储空间,将现有条目迁移到新位置。

对于包含 weak reference 的条目,迁移过程需要通过 ObjC runtime 的 objc_moveWeak 将 weak reference 从旧内存地址搬迁到新地址。这个操作涉及:

  1. _weak_unregister_no_lock:从 runtime 的 side table 中注销旧地址
  2. _weak_register_no_lock:在 side table 中注册新地址

单次操作很快,但如果某个通知名下积累了大量条目,扩容时需要逐个迁移所有 live entries 的 weak reference,累积耗时就可能达到秒级。

两种问题模式

NotificationCenter 的 entries 膨胀来自两个方面,它们可以独立存在,也可以叠加:

模式一:dead entries 累积(不调 removeObserver 的短生命周期对象)

当 observer 被释放时,其 weak reference 自动置 nil,但注册条目本身不会被移除。对于频繁创建和销毁的对象(如 Feed 中的各类 Component),如果不在 deinit 中 removeObserver,NotificationCenter 会持续累积 dead entries。

模式二:live entries 过多(长生命周期对象大量注册同一通知)

即使每个 observer 都正确管理了 removeObserver,如果大量长生命周期对象同时注册同一个通知,live entries 的数量本身就可能很大。

案例分析

回到开头的卡死案例。我们排查后发现,触发卡死的通知名 .tabBarDidChangeSelectedIndex 在整个 App 中有 31 个文件 注册了 observer,涵盖 Feed、社交、个人资料页、IM、Notice、电商 等几乎所有主要模块。

在 IM 模块的 会话 列表中,架构设计如下:

  • 每个会话对应一个持久化的 ViewModel(存储在字典中,不会被频繁销毁)
  • 每个 ViewModel 在 init 时创建一棵包含 50+ 子组件的组件树
  • 其中 ViewModel 本身和 TimeViewModel 各注册了一次 .tabBarDidChangeSelectedIndex

对于一个有 200 个会话的用户,仅 会话 列表就贡献了 400 个 live observers。这些 observer 的生命周期管理是正确的(dealloc 时通过组件树的 unmount 链路移除),但它们的数量本身就是问题。

再叠加 App 其他模块的 observer(包括可能存在的 dead entries),这个通知名下的总 entries 数量相当可观。当新增一个 observer 触发内部存储扩容时,迁移所有 entries 的累积耗时就造成了 13 秒的卡死。

被忽视的关键点

这个案例有一个容易被忽视的教训:即使 observer 生命周期管理完全正确(deinit 中有 removeObserver),也不意味着没有性能风险。 问题不在于单个 observer 的正确性,而在于同一个通知名下的 observer 总量。

哪些场景容易踩坑

1. 热门通知名 + 大量模块共同注册

像 Tab 切换、App 前后台、网络状态变化这类全局通知,往往被 App 中大量模块同时监听。每个模块的注册看起来都合理,但总量可能超出预期。

2. 持久化对象在 init 阶段无差别注册

如果一个对象会存在很久(如 1:1 对应数据模型的 ViewModel),且在 init 阶段就注册通知,那么所有实例的 observer 都会持续存在。即使只有屏幕上可见的几个实例真正需要响应通知,其余实例的注册也在白白增加 entries 总量。

3. 短生命周期对象不调 removeObserver

对于频繁创建和销毁的对象(如 Feed 滑动过程中的各种 Component),如果不在 deinit 中 removeObserver,每次销毁都会留下一个 dead entry。随着用户使用时间增长,dead entries 不断累积。

4. 组件树放大效应

在 TTKC 等组件化框架中,一个容器可能包含数十个子组件,每个子组件可能独立注册通知。容器的数量 × 子组件的数量 = 总 observer 数量,放大效应显著。

建议

按需注册:只为可见的实例注册 observer

对于列表中的 ViewModel,如果通知只用于更新 UI 展示(如刷新时间文本),那么只有屏幕上可见的实例才需要注册。可以在 Cell 即将显示时注册,在不可见时移除:

override func cellWillDisplay() {
    super.cellWillDisplay()
    NotificationCenter.default.addObserver(self, selector: #selector(onTabBarChange(_:)),
                                           name: .tabBarDidChangeSelectedIndex, object: nil)
}

override func cellDidEndDisplay() {
    super.cellDidEndDisplay()
    NotificationCenter.default.removeObserver(self, name: .tabBarDidChangeSelectedIndex, object: nil)
}

对于 200 个会话的用户,这将 observer 数量从 200 减少到 ~10-20(可见 Cell 数量)。

集中式 observer:N 个独立注册 → 1 个集中处理

如果多个同类对象都需要响应同一个通知,考虑用一个集中式 observer 替代 N 个独立注册:

// 在 DataController 中注册一次
NotificationCenter.default.addObserver(self, selector: #selector(onTabBarChange(_:)),
                                       name: .tabBarDidChangeSelectedIndex, object: nil)

@objc func onTabBarChange(_ notification: NSNotification) {
    for viewModel in viewModelDict.values {
        viewModel.handleTabBarChange()
    }
}

N 个 observer 注册变为 1 个,彻底消除了这个通知名下的数量问题。

对于短生命周期对象:在 deinit 中 removeObserver

deinit {
    NotificationCenter.default.removeObserver(self)
}

这一行代码的作用不是防 crash(iOS 9+ 不需要),而是及时清理 NotificationCenter 内部的注册条目,避免 dead entries 累积。

考虑使用 block-based API + 显式 token 管理

block-based API 返回一个 opaque token,移除时通过 token 精确定位,语义更清晰:

private var observerToken: NSObjectProtocol?

func setup() {
    observerToken = NotificationCenter.default.addObserver(
        forName: .someNotification, object: nil, queue: .main
    ) { [weak self] _ in
        self?.handleNotification()
    }
}

deinit {
    if let token = observerToken {
        NotificationCenter.default.removeObserver(token)
    }
}

需要注意的是,block-based API 不使用 zeroing weak reference——如果 block 中 strong capture 了 self,会导致循环引用。block 中必须使用 [weak self],且必须在合适时机 remove token。

在 Code Review 中关注

建议在 Code Review 中对以下模式保持敏感:

  • 这个通知名在 App 中有多少处注册?是否是"热门通知"?
  • 注册 observer 的对象有多少个实例同时存在?
  • 是否在 init 阶段就注册,但实际上只在可见时才需要?
  • 短生命周期对象是否在 deinit 中调了 removeObserver?

小结

NotificationCenter 的性能问题有两个维度:

  1. 单个 observer 的生命周期管理:短生命周期对象不调 removeObserver,导致 dead entries 累积
  2. 同一通知名下的 observer 总量:即使每个 observer 都正确管理了生命周期,大量 live observers 本身就是性能风险

第一个问题比较符合直觉,容易在 Code Review 中发现。第二个问题更隐蔽——每个模块的注册看起来都合理,但当一个大型 App 中有几十个模块同时注册同一个通知时,总量就可能超出 NotificationCenter 内部数据结构的性能安全边界。

Apple 的"不需要 removeObserver"是关于正确性的保证,不是关于性能的保证。在大型 App 中,NotificationCenter observer 需要像内存一样被视为一种有限资源来管理。

浏览器渲染原理与性能优化实战指南

作者 bluceli
2026年3月11日 10:27

引言

在现代前端开发中,理解浏览器渲染原理是进行性能优化的基础。本文将深入解析浏览器的渲染机制,并提供实用的性能优化技巧。

浏览器渲染流程

浏览器将HTML、CSS和JavaScript转换为可视页面的过程可以分为以下几个关键步骤:

  1. 构建DOM树:浏览器解析HTML文档,构建DOM(文档对象模型)树
  2. 构建CSSOM树:解析CSS样式,构建CSSOM(CSS对象模型)树
  3. 生成渲染树:将DOM和CSSOM合并,生成渲染树
  4. 布局(Layout):计算每个节点的几何信息(位置、大小)
  5. 绘制(Paint):将渲染树的各个节点绘制到屏幕上
  6. 合成(Composite):将各层合成,显示最终页面
// 监控渲染性能
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.duration);
  }
});

observer.observe({ entryTypes: ['layout', 'paint'] });

关键性能指标

理解关键性能指标对优化至关重要:

  • FCP(First Contentful Paint):首次内容绘制时间
  • LCP(Largest Contentful Paint):最大内容绘制时间
  • CLS(Cumulative Layout Shift):累积布局偏移
  • FID(First Input Delay):首次输入延迟
  • TTI(Time to Interactive):可交互时间
// 测量LCP
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

性能优化实战技巧

1. 减少重排和重绘

重排(Reflow)和重绘(Repaint)是性能杀手,应尽量避免:

// 不好的做法 - 导致频繁重排
const element = document.getElementById('box');
for (let i = 0; i < 100; i++) {
  element.style.left = i + 'px';
  element.style.top = i + 'px';
}

// 好的做法 - 批量修改样式
const element = document.getElementById('box');
element.style.transform = 'translate(100px, 100px)';

使用CSS transform代替top/left属性,因为transform不会触发重排。

2. 优化JavaScript执行

// 使用requestAnimationFrame优化动画
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

// 使用Web Worker处理复杂计算
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = function(e) {
  console.log('计算结果:', e.data);
};

3. 资源加载优化

<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="important.js" as="script">

<!-- 懒加载非关键资源 -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">

<!-- 使用defer和async -->
<script defer src="non-critical.js"></script>
<script async src="analytics.js"></script>

4. CSS优化

/* 使用will-change提示浏览器优化 */
.animated-element {
  will-change: transform;
  transform: translateZ(0);
}

/* 避免复杂选择器 */
/* 不好 */
.container div:nth-child(2n+1) .item span { }

/* 好 */
.item-even { }

5.虚拟滚动优化长列表

// 虚拟滚动实现示例
class VirtualScroll {
  constructor(container, itemHeight, totalItems) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.totalItems = totalItems;
    this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
    
    this.init();
  }
  
  init() {
    this.container.addEventListener('scroll', () => this.render());
    this.render();
  }
  
  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = Math.min(startIndex + this.visibleItems, this.totalItems);
    
    // 只渲染可见区域的元素
    this.container.innerHTML = '';
    for (let i = startIndex; i < endIndex; i++) {
      const item = this.createItem(i);
      item.style.position = 'absolute';
      item.style.top = (i * this.itemHeight) + 'px';
      this.container.appendChild(item);
    }
  }
  
  createItem(index) {
    const div = document.createElement('div');
    div.textContent = `Item ${index}`;
    div.className = 'scroll-item';
    return div;
  }
}

性能监控工具

使用Chrome DevTools进行性能分析:

// 使用Performance API监控
const perfData = performance.getEntriesByType('navigation')[0];
console.log('页面加载时间:', perfData.loadEventEnd - perfData.fetchStart);

// 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('长任务:', entry.duration, 'ms');
  }
});
longTaskObserver.observe({ entryTypes: ['longtask'] });

实用性优化建议

  1. 图片优化:使用WebP格式,实现响应式图片
  2. 代码分割:使用动态import()按需加载
  3. 缓存策略:合理使用Service Worker和HTTP缓存
  4. CDN加速:静态资源使用CDN分发
  5. Gzip压缩:启用服务器端压缩
// 动态导入示例
async function loadModule() {
  const module = await import('./heavyModule.js');
  module.doSomething();
}

// 图片懒加载观察器
const imgObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      imgObserver.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imgObserver.observe(img);
});

总结

浏览器性能优化是一个系统工程,需要从多个维度综合考虑:

  • 理解渲染原理,避免不必要的重排重绘
  • 优化资源加载策略,提升首屏加载速度
  • 合理使用JavaScript特性,避免阻塞主线程
  • 持续监控性能指标,及时发现和解决问题

记住,性能优化应该基于实际测量数据,而不是凭感觉。使用Chrome DevTools等工具进行性能分析,针对性地解决瓶颈问题,才能获得最佳的优化效果。

通过掌握这些渲染原理和优化技巧,你将能够构建出更加流畅、高效的前端应用,为用户提供更好的使用体验。

Web性能测试流程全解析:从概念到落地的完整指南

作者 LeonGao
2026年3月11日 10:26

在互联网高速发展的今天,Web应用已成为企业服务用户、开展业务的核心载体,其性能表现直接决定用户体验、留存率乃至企业营收。试想,当用户访问一个电商网站时,页面加载超过3秒就可能导致70%的用户流失;当直播平台遭遇峰值流量冲击时,卡顿、崩溃会直接影响品牌口碑。而Web性能测试,正是保障Web应用稳定、高效运行的关键手段——它通过科学的流程、标准化的方法,模拟真实用户场景,发现应用性能瓶颈,为优化提供数据支撑。本文将从概念入手,详细拆解Web性能测试的完整流程,结合理论背景与实操细节,帮助读者全面掌握这一核心技术,解决实际应用中的性能难题。

一、Web性能测试的核心背景与定义

1.1 背景:为什么Web性能测试不可或缺?

随着Web应用的功能日益复杂,用户规模不断扩大,影响Web性能的因素也愈发多元:前端页面的资源体积、后端接口的响应速度、数据库的查询效率、服务器的负载能力、网络带宽的稳定性,甚至第三方插件的性能,都可能成为性能瓶颈。在实际应用中,很多企业往往只关注功能是否正常,忽视了性能测试,导致应用上线后出现一系列问题:高峰期页面加载缓慢、接口超时、并发用户过多时系统崩溃、不同终端(PC端、移动端)性能差异显著等。这些问题不仅会降低用户体验,还可能造成直接的经济损失——例如,电商平台在大促期间因性能问题无法正常下单,每一分钟的故障都可能损失数十万元营收。

此外,随着5G、物联网等技术的普及,用户对Web应用的性能要求也不断提升:页面加载时间需控制在2秒以内,接口响应时间不超过500ms,并发用户支持能力需满足业务峰值需求。在此背景下,Web性能测试已不再是“可选环节”,而是贯穿Web应用开发、测试、上线、运维全生命周期的“必选动作”,成为保障应用质量的核心防线。

1.2 定义:什么是Web性能测试?

Web性能测试是指通过模拟真实用户行为、模拟不同的网络环境和负载场景,对Web应用的性能指标进行量化检测、分析和评估的过程。其核心目标是:发现应用的性能瓶颈,验证应用在不同负载下的稳定性和可靠性,确保应用能够满足预期的性能需求,为性能优化提供数据依据。

与功能测试不同,Web性能测试不关注“功能是否可用”,而关注“功能在不同场景下的性能表现”——例如,同样是“用户登录”功能,功能测试验证的是“输入正确账号密码能否成功登录”,而性能测试验证的是“1000个用户同时登录时,登录接口的响应时间是多少、服务器CPU和内存占用率如何、是否会出现登录失败的情况”。

Web性能测试的核心对象包括:前端页面(HTML、CSS、JavaScript、图片、视频等静态资源)、后端接口(API接口、微服务接口)、数据库、服务器(Web服务器、应用服务器)以及网络环境(带宽、延迟、丢包率)。

二、Web性能测试的完整流程:从准备到落地

Web性能测试并非“一次性操作”,而是一个标准化、系统化的流程,通常分为6个核心阶段:需求分析与场景定义、测试环境搭建、测试计划制定、测试脚本开发、测试执行与监控、测试结果分析与优化。每个阶段环环相扣,缺一不可,确保测试结果的准确性、可靠性和实用性。

2.1 阶段一:需求分析与场景定义(核心前提)

需求分析是Web性能测试的第一步,也是最关键的一步——如果需求不明确、场景定义不合理,后续的测试工作都将失去意义。此阶段的核心目标是明确“测试什么”“测试场景是什么”“预期指标是什么”,具体分为3个步骤:

  1. 明确业务需求:与产品、开发、运维等相关方沟通,了解Web应用的核心业务场景(如电商的“浏览商品-加入购物车-下单-支付”、社交平台的“发布内容-评论-点赞”)、用户规模(日均活跃用户、峰值活跃用户)、业务峰值时段(如电商大促、直播带货高峰)以及用户分布(不同地区、不同网络环境)。

  2. 定义性能指标:根据业务需求,确定核心性能指标,分为前端性能指标、后端性能指标和系统资源指标三类,具体如下: 示例:某电商网站的性能指标要求为:首屏加载时间≤2秒,接口平均响应时间≤500ms,90%接口响应时间≤800ms,TPS≥1000,并发用户数支持≥5000,服务器CPU占用率≤70%,内存占用率≤80%,错误率≤0.1%。

    1. 前端性能指标:页面加载时间(首屏加载时间、白屏时间、完全加载时间)、资源加载时间(CSS、JS、图片加载时间)、DOM渲染时间、页面交互响应时间(如按钮点击后反馈时间)。
    2. 后端性能指标:接口响应时间(平均响应时间、90%响应时间、99%响应时间)、接口吞吐量(TPS,每秒处理的请求数)、并发用户数(同时在线并进行操作的用户数)、错误率(请求失败的比例)。
    3. 系统资源指标:服务器CPU占用率、内存占用率、磁盘IO、网络带宽占用率;数据库查询响应时间、连接数、锁等待时间。
  3. 设计测试场景:根据业务场景和性能指标,设计不同的测试场景,覆盖正常负载、峰值负载、极限负载等多种情况,确保测试结果能够反映应用在真实环境中的性能表现。常见的测试场景包括:

    1. 正常负载场景:模拟日均活跃用户的操作行为,验证应用在正常流量下的性能表现。
    2. 峰值负载场景:模拟业务峰值时段的用户行为(如电商大促、直播高峰),验证应用在高并发下的稳定性。
    3. 极限负载场景:逐步增加并发用户数,直到应用出现性能瓶颈(如接口超时、系统崩溃),确定应用的最大承载能力。
    4. 稳定性场景:在正常负载或峰值负载下,持续运行一段时间(如24小时、72小时),验证应用的长期稳定性,是否会出现内存泄漏、资源耗尽等问题。
    5. 网络场景:模拟不同的网络环境(如4G、5G、WiFi、弱网),验证应用在不同网络条件下的性能表现。

2.2 阶段二:测试环境搭建(保障测试准确性)

测试环境是Web性能测试的基础,环境的合理性直接影响测试结果的准确性——如果测试环境与生产环境差异过大,测试结果将失去参考价值。此阶段的核心目标是搭建一套与生产环境“尽可能一致”的测试环境,具体包括以下4个方面:

  1. 硬件环境搭建

    1. 服务器:采用与生产环境相同配置的Web服务器(如Nginx、Apache)、应用服务器(如Tomcat、Jetty)、数据库服务器(如MySQL、Oracle),包括CPU、内存、磁盘、网络带宽等参数一致。
    2. 测试机:用于运行测试工具、模拟用户行为的机器,配置需满足测试工具的运行要求,避免因测试机性能不足影响测试结果。
  2. 软件环境搭建

    1. 操作系统:服务器和测试机的操作系统与生产环境一致(如Linux、Windows Server)。
    2. 应用版本:部署与生产环境相同版本的Web应用,包括前端代码、后端服务、数据库脚本等,确保应用功能与生产环境一致。
    3. 依赖组件:安装与生产环境相同版本的依赖组件(如JDK、Python、数据库驱动等),避免因组件版本差异导致性能问题。
  3. 网络环境模拟

    1. 使用网络模拟工具(如Charles、Fiddler、JMeter的网络延迟模拟功能),模拟不同的网络环境,包括带宽限制、延迟、丢包率等,还原真实用户的网络场景。
    2. 确保测试环境与应用服务器、数据库服务器之间的网络连接稳定,避免网络瓶颈影响测试结果。
  4. 环境隔离:测试环境需与生产环境、开发环境隔离,避免测试过程中对其他环境造成影响,同时防止其他环境的流量干扰测试结果。可以通过防火墙、虚拟网络等方式实现环境隔离。

示例:某Web应用的生产环境为:Linux服务器(CPU:8核,内存:16GB,磁盘:1TB,带宽:100Mbps),Web服务器为Nginx,应用服务器为Tomcat,数据库为MySQL 8.0;测试环境则完全沿用该配置,同时使用JMeter模拟4G、5G、弱网(带宽1Mbps,延迟100ms,丢包率5%)等网络场景。

2.3 阶段三:测试计划制定(明确测试方案)

测试计划是Web性能测试的“行动指南”,用于明确测试的范围、目标、资源、进度、风险等,确保测试工作有序推进。测试计划通常包括以下核心内容:

  1. 测试概述:简要介绍测试的目的、范围、测试对象(前端、后端、数据库等),以及测试的重要性。

  2. 测试目标:明确本次测试需要达成的目标,如验证应用在峰值负载下的性能是否满足预期指标、发现应用的性能瓶颈、评估应用的最大承载能力等。

  3. 测试范围:明确测试的功能模块(如登录模块、商品浏览模块、下单模块)、测试场景(正常负载、峰值负载等)、测试指标(如响应时间、TPS等),以及不测试的内容(如功能正确性测试)。

  4. 测试资源

    1. 人力资源:测试负责人、性能测试工程师、开发工程师、运维工程师的职责分工。
    2. 硬件资源:测试环境的服务器、测试机配置。
    3. 软件资源:测试工具(如JMeter、LoadRunner、Chrome DevTools等)、监控工具(如Prometheus、Grafana、Nagios等)。
  5. 测试进度安排:明确每个阶段的时间节点、任务内容、责任人,确保测试工作按时完成。例如:需求分析与场景定义(1天)、测试环境搭建(2天)、测试脚本开发(3天)、测试执行与监控(2天)、测试结果分析与优化建议(1天)。

  6. 测试风险与应对措施:预判测试过程中可能出现的风险(如测试环境搭建失败、测试脚本开发受阻、测试结果异常等),并制定相应的应对措施。例如:测试环境搭建失败,安排运维工程师协助排查,延长1天搭建时间;测试结果异常,检查测试脚本和环境配置,重新执行测试。

  7. 测试标准:明确测试通过的标准,即各项性能指标需达到的预期值,以及测试失败的处理流程(如重新测试、优化应用后再测试)。

2.4 阶段四:测试脚本开发(模拟用户行为)

测试脚本是模拟用户行为的核心,用于向Web应用发送请求、模拟用户操作(如点击、输入、跳转),并记录性能数据。常用的Web性能测试工具包括JMeter、LoadRunner、Gatling等,其中JMeter因开源、易用、功能强大,成为最常用的工具之一。本节将以JMeter为例,详细介绍测试脚本的开发流程和核心技巧。

2.4.1 测试脚本开发的核心步骤

  1. 新建测试计划:打开JMeter,新建一个测试计划,命名为“Web性能测试计划”,设置测试计划的基本信息(如备注、用户定义的变量等)。

  2. 添加线程组:线程组是模拟用户的核心,用于设置并发用户数、循环次数、测试持续时间等。右键点击测试计划,选择“添加-线程(用户)-线程组”,设置参数:

    1. 线程数:并发用户数(如500、1000)。
    2. Ramp-Up时间(秒):线程启动的时间间隔,即多少秒内启动所有线程(如10秒启动1000个线程,即每秒启动100个线程)。
    3. 循环次数:每个线程执行的次数(如10次),也可设置“永远”,配合测试持续时间使用。
    4. 测试持续时间(秒):测试的总时长(如300秒),适用于稳定性测试。
  3. 添加HTTP请求:根据测试场景,添加HTTP请求,模拟用户对Web应用的请求(如访问首页、登录、浏览商品等)。右键点击线程组,选择“添加-取样器-HTTP请求”,设置参数:

    1. 协议:HTTP或HTTPS。
    2. 服务器名称或IP:Web应用的服务器IP或域名。
    3. 端口号:Web应用的端口(如80、443)。
    4. 请求方法:GET(获取资源)、POST(提交数据)等。
    5. 路径:请求的接口路径(如“/index”“/api/login”)。
    6. 参数:请求的参数(如登录时的账号、密码),可在“参数”选项卡中添加。
  4. 添加配置元件:用于设置请求的公共参数、Cookie、请求头、缓存等,避免重复配置。常用的配置元件包括:

    1. HTTP请求默认值:设置所有HTTP请求的公共参数(如服务器IP、端口、协议),减少重复配置。
    2. HTTP Cookie管理器:用于管理Cookie,模拟用户登录后的会话状态(如登录后获取Cookie,后续请求携带Cookie)。
    3. HTTP请求头管理器:设置请求头(如User-Agent、Content-Type),模拟不同浏览器、不同终端的请求。
  5. 添加断言:用于验证请求的响应是否正确,确保测试脚本模拟的行为有效。例如,登录请求后,断言响应中包含“登录成功”字样,说明登录请求执行成功。右键点击HTTP请求,选择“添加-断言-响应断言”,设置断言条件(如响应文本包含指定内容)。

  6. 添加监听器:用于收集和展示测试数据,如响应时间、TPS、错误率等。常用的监听器包括:

    1. 查看结果树:查看每个请求的详细信息(请求参数、响应内容、响应时间),用于调试脚本。
    2. 聚合报告:展示核心性能指标(平均响应时间、90%响应时间、TPS、错误率等),用于分析测试结果。
    3. 图形结果:以图表形式展示响应时间、吞吐量等指标的变化趋势,直观反映应用性能。
  7. 脚本调试与优化:脚本开发完成后,先设置少量线程数(如1个线程),执行脚本,通过查看结果树排查脚本中的问题(如请求失败、断言失败),优化脚本(如调整参数、添加Cookie、修改请求头),确保脚本能够正常运行。

2.4.2 测试脚本开发示例(登录接口性能测试)

以下是使用JMeter开发登录接口性能测试脚本的具体步骤和代码示例(简化版):

  1. 新建测试计划,命名为“登录接口性能测试”。
  2. 添加线程组,设置线程数为500,Ramp-Up时间为10秒,循环次数为10次。
  3. 添加HTTP请求默认值,设置协议为HTTPS,服务器名称为“www.example.com”,端口为443。
  4. 添加HTTP Cookie管理器,用于管理登录后的会话。
  5. 添加HTTP请求,设置路径为“/api/login”,请求方法为POST,参数如下: 名称值编码usernametestuserUTF-8password123456UTF-8
  6. 添加响应断言,设置“响应文本包含”“登录成功”。
  7. 添加聚合报告和查看结果树监听器。
  8. 脚本调试:设置线程数为1,执行脚本,查看结果树,确认登录请求响应成功,断言通过。

此外,为了模拟真实用户的随机性,还可以使用JMeter的“用户定义的变量”“CSV数据文件设置”等元件,从CSV文件中读取不同的账号密码,模拟多个不同用户的登录行为。CSV文件示例(user.csv):

username,password
testuser1,123456
testuser2,654321
testuser3,abcdef
...

在JMeter中添加“CSV数据文件设置”元件,设置文件名为“user.csv”,变量名为“username,password”,即可实现多用户随机登录。

2.5 阶段五:测试执行与监控(收集性能数据)

测试脚本开发完成并调试通过后,进入测试执行阶段。此阶段的核心目标是按照测试计划和测试场景,执行测试脚本,同时监控系统资源和性能指标,收集完整的测试数据。具体分为以下3个步骤:

2.5.1 测试执行前准备

  1. 检查测试环境:确认测试环境的服务器、应用、数据库、网络等均正常运行,与生产环境配置一致。
  2. 检查测试脚本:确认测试脚本无错误,断言设置正确,监听器能够正常收集数据。
  3. 清理测试环境:清除数据库中的测试数据、服务器缓存、日志文件等,避免历史数据影响测试结果。
  4. 启动监控工具:启动系统资源监控工具(如Prometheus、Grafana)、数据库监控工具(如MySQL Monitor)、网络监控工具(如Wireshark),确保能够实时监控服务器CPU、内存、磁盘IO、网络带宽,以及数据库、应用的运行状态。

2.5.2 测试执行过程

按照测试场景的顺序,依次执行测试脚本,过程中需注意以下几点:

  1. 逐步增加负载:执行峰值负载和极限负载测试时,不要一次性启动大量线程,应逐步增加线程数(如从100、200、500、1000逐步增加),观察性能指标的变化,避免瞬间负载过大导致系统崩溃。
  2. 实时监控数据:测试执行过程中,实时查看监听器中的性能数据(响应时间、TPS、错误率)和监控工具中的系统资源数据,记录异常情况(如响应时间突然飙升、错误率增加、服务器CPU占用率过高)。
  3. 保持测试环境稳定:测试执行期间,避免对测试环境进行其他操作(如部署应用、修改配置),防止干扰测试结果。
  4. 重复测试:对于关键场景(如峰值负载、稳定性测试),建议重复执行2-3次,确保测试结果的稳定性和可靠性。

示例:执行电商网站峰值负载测试,线程数从500逐步增加到5000,每增加1000个线程,稳定运行30秒,记录不同线程数下的响应时间、TPS、服务器CPU和内存占用率,以及错误率的变化。

2.5.3 测试执行后操作

  1. 停止测试脚本:测试执行完成后,正常停止测试脚本,避免强制停止导致数据丢失。
  2. 收集测试数据:导出监听器中的测试数据(如聚合报告、图形结果),以及监控工具中的系统资源数据,整理成测试数据集。
  3. 恢复测试环境:清理测试数据,关闭监控工具,将测试环境恢复到初始状态,为后续测试或其他工作提供便利。

2.6 阶段六:测试结果分析与优化建议(核心输出)

测试执行完成后,进入测试结果分析阶段——这是Web性能测试的核心环节,通过分析测试数据,发现性能瓶颈,找出问题根源,并给出针对性的优化建议。此阶段分为4个步骤:

2.6.1 数据整理与对比

将收集到的测试数据(性能指标、系统资源数据)进行整理,与预期性能指标进行对比,判断测试是否通过。例如:

  • 若接口平均响应时间为450ms,预期为≤500ms,说明该指标达标;若平均响应时间为600ms,则不达标,需要分析原因。
  • 若TPS为1200,预期为≥1000,说明达标;若TPS为800,则不达标。

同时,整理不同测试场景下的数据,对比分析性能变化规律(如并发用户数增加时,响应时间如何变化、TPS如何变化)。

2.6.2 性能瓶颈定位

通过分析测试数据和监控数据,定位性能瓶颈的位置。常见的性能瓶颈主要分为以下4类,定位方法如下:

  1. 前端瓶颈

    1. 现象:页面加载时间过长、资源加载缓慢、DOM渲染延迟。
    2. 定位方法:使用Chrome DevTools的“Network”面板,查看各静态资源(CSS、JS、图片)的加载时间,找出加载时间过长的资源;使用“Performance”面板,查看DOM渲染、JS执行的时间,定位瓶颈。
  2. 后端接口瓶颈

    1. 现象:接口响应时间过长、TPS过低、错误率高。
    2. 定位方法:查看JMeter聚合报告,找出响应时间过长的接口;使用接口监控工具(如Postman、Swagger)单独测试该接口,排查接口本身的问题;查看应用服务器日志,分析接口执行过程中的异常(如代码报错、逻辑复杂导致执行时间长)。
  3. 数据库瓶颈

    1. 现象:接口响应时间不稳定、查询缓慢、数据库连接数过高、锁等待时间长。
    2. 定位方法:使用数据库监控工具,查看数据库的查询响应时间、连接数、锁等待情况;分析慢查询日志,找出执行效率低的SQL语句;检查数据库索引是否合理、表结构是否优化。
  4. 服务器与网络瓶颈

    1. 现象:服务器CPU、内存占用率过高,磁盘IO繁忙,网络带宽占用率过高;接口响应时间受网络环境影响较大。
    2. 定位方法:使用服务器监控工具,查看CPU、内存、磁盘IO、网络带宽的占用情况;使用网络监控工具,查看网络延迟、丢包率,定位网络瓶颈。

示例:通过测试数据发现,当并发用户数达到3000时,接口平均响应时间从450ms飙升至1200ms,TPS从1200下降至500,同时服务器CPU占用率达到90%,内存占用率达到85%。由此可定位瓶颈为服务器资源不足,无法承载3000以上的并发用户。

2.6.3 问题根源分析

定位到性能瓶颈后,进一步分析问题的根源,明确导致瓶颈的具体原因。例如:

  • 前端资源加载缓慢的原因:图片未压缩、JS/CSS未合并、未使用CDN加速、资源缓存策略不合理。
  • 接口响应时间过长的原因:代码逻辑复杂、未使用缓存、接口调用次数过多、参数传递不合理。
  • 数据库查询缓慢的原因:SQL语句未优化、未建立索引、表数据量过大、数据库配置不合理。
  • 服务器资源不足的原因:服务器配置过低、未进行负载均衡、应用未进行集群部署。

2.6.4 给出优化建议

根据问题根源,给出针对性的优化建议,建议需具体、可落地,同时明确优化后的预期效果。常见的优化建议如下:

  1. 前端优化

    1. 图片优化:压缩图片(使用WebP格式)、懒加载图片,减少图片资源体积。
    2. JS/CSS优化:合并JS/CSS文件、压缩代码(去除冗余代码)、使用异步加载JS,减少资源加载时间。
    3. 缓存优化:设置合理的资源缓存策略(如HTTP缓存、本地存储),减少重复请求。
    4. CDN加速:使用CDN分发静态资源,缩短用户访问资源的距离,提高加载速度。
  2. 后端接口优化

    1. 代码优化:简化复杂逻辑、减少不必要的接口调用、使用异步处理(如异步任务、消息队列),提高接口执行效率。
    2. 缓存优化:使用Redis、Memcached等缓存工具,缓存常用数据(如用户信息、商品信息),减少数据库查询次数。
    3. 接口设计优化:合并相似接口、减少参数传递、使用分页查询,降低接口负载。
  3. 数据库优化

    1. SQL优化:优化慢查询语句(如避免全表扫描、使用索引、优化JOIN语句)。
    2. 索引优化:为常用查询字段建立索引,提高查询效率;定期维护索引,避免索引失效。
    3. 数据库配置优化:调整数据库连接数、缓存大小、查询超时时间等参数,优化数据库性能。
    4. 分库分表:当表数据量过大时,采用分库分表(水平分表、垂直分表),减轻单表压力。
  4. 服务器与网络优化

    1. 服务器配置升级:增加CPU、内存、磁盘容量,提高服务器承载能力。
    2. 负载均衡:使用Nginx、HAProxy等负载均衡工具,将流量分发到多个应用服务器,分担单服务器压力。
    3. 集群部署:将应用、数据库部署为集群,提高系统的可用性和承载能力。
    4. 网络优化:提升网络带宽、优化网络路由,减少网络延迟和丢包率。

示例:针对服务器资源不足的瓶颈,给出优化建议:1. 升级服务器配置(CPU从8核升级为16核,内存从16GB升级为32GB);2. 部署应用集群(增加2台应用服务器),使用Nginx实现负载均衡;3. 优化应用代码,减少服务器资源占用。优化后预期:并发用户数支持≥5000,接口平均响应时间≤500ms,服务器CPU占用率≤70%。

三、Web性能测试的优缺点分析与实际应用建议

3.1 优点

  1. 提前发现性能瓶颈:通过模拟真实场景,在应用上线前发现性能问题,避免上线后出现故障,减少经济损失和品牌影响。
  2. 量化性能指标:通过科学的测试方法,量化Web应用的性能表现(如响应时间、TPS、并发用户数),为性能优化提供数据支撑,避免“凭经验优化”。
  3. 保障用户体验:通过性能优化,提升Web应用的加载速度和响应效率,改善用户体验,提高用户留存率和转化率。
  4. 降低运维成本:提前解决性能问题,减少应用上线后的运维成本(如故障排查、服务器扩容),提高系统的稳定性和可靠性。
  5. 支撑业务发展:通过测试评估应用的最大承载能力,为业务扩张(如用户增长、活动推广)提供决策依据,确保应用能够满足业务发展需求。

3.2 缺点

  1. 测试环境搭建复杂:需要搭建与生产环境一致的测试环境,包括硬件、软件、网络等,耗时耗力,尤其是对于大型Web应用,环境搭建难度更大。
  2. 测试成本较高:需要投入人力(性能测试工程师)、物力(服务器、测试机)、财力(测试工具、监控工具),尤其是极限负载测试和长期稳定性测试,成本较高。
  3. 测试结果存在偏差:即使测试环境与生产环境高度一致,也无法完全模拟真实用户的行为(如用户的操作习惯、网络环境的随机性),导致测试结果与生产环境的实际性能存在一定偏差。
  4. 技术门槛较高:Web性能测试需要掌握测试工具(如JMeter)、监控工具、数据库优化、服务器优化等相关知识,对测试工程师的技术能力要求较高。
  5. 测试周期较长:完整的Web性能测试流程(需求分析、环境搭建、脚本开发、测试执行、结果分析)需要一定的时间,尤其是稳定性测试(如72小时持续测试),会延长项目周期。

3.3 实际应用建议

  1. 结合业务优先级开展测试:优先测试核心业务场景(如电商的下单支付、社交平台的登录发布),对于非核心场景,可以适当降低测试优先级,减少测试成本和周期。
  2. 优化测试环境,减少偏差:尽可能使测试环境与生产环境保持一致,包括服务器配置、应用版本、网络环境、用户数据量等;同时,增加测试场景的多样性,模拟不同用户的操作行为和网络环境,减少测试结果的偏差。
  3. 选择合适的测试工具:根据项目规模和需求选择测试工具,小型项目可使用开源工具(如JMeter、Chrome DevTools),大型项目可使用商业工具(如LoadRunner),同时结合监控工具(如Prometheus、Grafana),提高测试效率和数据准确性。
  4. 将性能测试融入全生命周期:不要等到应用开发完成后才进行性能测试,应将性能测试融入开发、测试、上线、运维全生命周期——例如,开发阶段进行单元性能测试,测试阶段进行集成性能测试,上线后进行常态化性能监控,及时发现和解决性能问题。
  5. 注重测试结果的落地:测试的最终目的是优化应用性能,因此,测试结果分析后,需推动开发、运维等相关方落实优化建议,并对优化后的应用进行回归测试,验证优化效果,形成“测试-分析-优化-回归”的闭环。
  6. 培养专业的性能测试团队:加强对测试工程师的技术培训,提升其在测试工具使用、性能瓶颈定位、优化方案设计等方面的能力,确保性能测试工作的专业性和有效性。

四、结论:Web性能测试的价值与未来发展

4.1 核心价值总结

Web性能测试是保障Web应用稳定、高效运行的核心手段,其核心价值在于:通过标准化的流程和科学的方法,提前发现性能瓶颈,量化性能指标,为性能优化提供数据支撑,最终提升用户体验、降低运维成本、支撑业务发展。在互联网竞争日益激烈的今天,Web应用的性能已成为企业的核心竞争力之一,而Web性能测试,正是企业打造高性能Web应用的“必经之路”。

无论是小型个人网站,还是大型企业级Web应用,都需要重视Web性能测试——小型网站通过性能测试,可避免因性能问题导致用户流失;大型企业级应用通过性能测试,可保障在高并发、高负载场景下的稳定性,避免因性能故障造成重大经济损失。

4.2 未来发展趋势

随着Web技术的不断发展(如微服务、云原生、人工智能、5G等),Web性能测试也将呈现出以下几个发展趋势:

  1. 自动化与智能化:未来,Web性能测试将更加自动化,通过脚本自动化生成、测试用例自动化设计、测试执行自动化、结果分析自动化,减少人工干预;同时,结合人工智能技术,实现性能瓶颈的智能定位、优化方案的智能推荐,提高测试效率和准确性。
  2. 云原生场景适配:随着云原生技术的普及,越来越多的Web应用部署在云平台(如阿里云、腾讯云),未来的Web性能测试将更加注重云原生场景的适配,支持容器化、微服务架构的性能测试,模拟云环境下的高并发、弹性伸缩等场景。
  3. 全链路性能测试:传统的Web性能测试往往聚焦于单个模块(如前端、后端),未来将向全链路性能测试发展,覆盖从用户请求发起、CDN分发、负载均衡、应用服务、数据库到响应返回的全链路,全面定位全链路中的性能瓶颈。
  4. 实时性能监控与预警:未来,Web性能测试将与常态化监控结合,实现实时性能监控和异常预警——通过监控工具实时采集性能数据,当性能指标超出阈值时,自动发出预警,及时发现和解决上线后的性能问题,保障应用的持续稳定运行。
  5. 多终端、多场景适配:随着移动互联网的发展,用户访问Web应用的终端(PC端、移动端、平板端)和场景(不同网络、不同地域)越来越多样化,未来的Web性能测试将更加注重多终端、多场景的适配,确保Web应用在不同终端和场景下都能有良好的性能表现。

五、参考资料(可选)

  1. 《JMeter实战指南》(作者:林均鹏):详细介绍JMeter的使用方法和性能测试实战技巧。
  2. 《Web性能测试与优化》(作者:张立华):涵盖Web性能测试的流程、工具、优化方法,适合入门学习。
  3. JMeter官方文档:jmeter.apache.org/documentati…
  4. Chrome DevTools官方文档:developer.chrome.com/docs/devtoo…
  5. 《高性能MySQL》(作者:Baron Schwartz):深入讲解MySQL数据库的性能优化技巧,适用于数据库瓶颈定位与优化。
  6. Prometheus官方文档:prometheus.io/docs/,介绍系统资…

Webpack5基础与高级配置

作者 willow
2026年3月10日 22:47

为什么需要打包工具?

  1. 编译功能:我们在项目使用的框架、less、sass、es6等浏览器都不能识别,需要打包工具编译成css js等语法才能运行
  2. 还可以压缩代码、做兼容性处理、提高代码性能
  3. 主要的打包工具:webpack vite rollup

对webpack的理解?

  1. 是一个静态资源打包工具,它以一个或多个文件作为打包的入口,将整个项目的文件编译成一个或多个文件输出出去,输出文件bundle就是编译好的文件,浏览器可以运行。本身功能有限制,只能编译js的ES Module语法.
  2. 可以做什么?
    ① 编译代码,编译高级的语言或语法(TS、ES6+、sass等);
    ② 模块整合,在项目中有很多模块和文件,合并之后可以,解决浏览器频繁请求文件的问题;
    ③ 兼容性和错误检查(Polyfill、postcss、eslint);
    ④ 代码体积更小,加载更快(Tree-shaking、压缩、合并

webpack怎么使用

  1. 安装webpack 初始化package.json npm init -y
  2. 启用webpack:npx webpack ./src/main.js --mode=development,指定Webpack从main.js文件开始打包,并且打包 main.js其依赖也一起打包进来。
  3. npx是通过检查本地缓存或临时下载的包来执行命令,执行后会清理临时文件。首先它会在当前目录下的node_modules.bin文件去查找是否有可执行的命令,如果没有就从全局查找是否有安装对应的模块,全局也没有就会自动下载对应的模块,用完就删。
  4. 总结:webpack本身功能比较少,只能处理js资源,一旦遇到css等其他资源就会报错。所以学习webpack,就是主要学习如何处理其他资源。

常见的Loader?

  1. css-loader:加载css,支持模块化
  2. style-loader:将css添加到页面的style标签里面
  3. postcss-loader:css的兼容性处理
  4. less-loader:处理less文件
  5. file-loader:把文件输出到文件夹,代码可以通过url去引用文件
  6. url-loader:和file-loader类似,但是能在⽂件很⼩的情况下以base64的⽅式把⽂件内容注⼊到代码中去
  7. babel-loader:转换es6为es5
  8. eslint-loader:通过 ESLint 检查 JavaScript 代码
  9. vue-loader:处理vue文件

常见的Plugin?

  1. DefinePlugin: 定义环境变量,内置插件
  2. clean-webpack-plugin: 清理构建目录
  3. html-webpack-plugin: 打包后自动生成一个html文件,并把打包生成的js自动引入到模块
  4. mini-css-extract-plugin:将CSS提取到单独的⽂件中
  5. css-minimizer-webpack-plugin: 压缩css,代替webpack4的optimize-css-assets-webpack-plugin
  6. terser-webpack-plugin:压缩js,代替webpack4的uglifyjs-webpack-plugin
  7. copy-webpack-plugin:复制静态文件到输出目录
  8. image-minimizer-webpack-plugin:自动压缩图片

基础配置

核心概念
  1. entry(入口):提示文件从哪里打包
  2. output(输出):提示文件打包完的文件输出到哪里去,如何命名
  3. loader(加载器):webpack本身只能处理js资源,需要借助loader处理其他资源
  4. plugins(插件):扩展webpack功能
  5. mode(模式):分为开发模式与生产模式;
开发模式与生产模式
  1. 开发模式主要关注:① 编译代码,使浏览器能识别运行。图片、样式、字体、html资源等,webpack默认都不能处理这些资源,所以需要配置来编译这些资源;②检查代码质量、代码规范,统一团队的编码规范、让代码更加优雅美观。
  2. 生产模式需要代码将来部署上线,主要关注:优化代码运行性能;优化代码打包速度
处理css资源
开发模式
  1. 安装插件:less-loader css-loader style-loader
  2. less-loader: 将less文件编译成css文件
  3. css-loader:将css文件编译成webpack可以识别的模块
  4. style-loader:通过动态创造一个style标签,把css添加到html页面上
  5. 使用,从右到左执行loader:rules: [{test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"]} ]
生产模式
  1. mini-css-extract-plugin:提取css为单独文件。在开发模式css文件被打包到js文件中,当js文件加载时,会创建一个style标签来生成样式。但是在生产模式实际打开有可能会出现闪屏现象,用户体验不好,新建单独的css文件,通过link标签加载性能才好。
  2. 使用:提取css成单独文件,定义输出文件名和目录:plugins: [new MiniCssExtractPlugin({filename: "static/css/main.css",})]
  3. postcss-loader:css兼容性处理,下载插件:npm i postcss-loader postcss postcss-preset-env -D,然后在 package.json 文件中添加 browserslist 来控制样式的兼容性做到什么程度;
  4. css-minimizer-webpack-plugin:压缩css,使用plugins: [new CssMinimizerPlugin()]
module: {
  rules: [{
    test: /\.less$/,
    //从右到左 less-loader postcss-loader css-loader MiniCssExtractPlugin.loader
    use: [
      // 1 生产环境style-loader替换成 MiniCssExtractPlugin.loader,
      MiniCssExtractPlugin.loader,
      "css-loader",
      {
        loader: "postcss-loader",
        options: {
          // 2 postcss-loader 能解决大多数样式兼容性问题
          postcssOptions: {
            plugins: [
              "postcss-preset-env",
            ],
          },
        },
      },
      // 3 将less文件编译成css文件
      "less-loader",
    ],
  ]}
},
plugins: [
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/main.css",
    }),
    // css压缩
    new CssMinimizerPlugin(),
  ],
处理图片资源
  1. 在 Webpack4 时,我们处理图片资源通过file-loader url-loader进行处理;
  2. Webpack5已经将两个Loader功能内置到Webpack里了,只需简单配置即可处理图片资源rules: [{test:/.[png|jpe?g|gif|webp]$/, type: "asset"},]
  3. type: "asset" 相当于url-loader, 将文件转化成Webpack能识别的资源,同时小于某个大小的资源会处理成data URI形式;
  4. 将小于10kb的图片转为base64格式, 优点是减少请求数量。rules: [{test:/\.[png|jpe?g|gif|webp]$/, type: "asset"},perser:{dataUrlCondition:maxSize: 10 *1024}]
处理字体图标、其他资源如音视频
  1. 阿里巴巴矢量图标库添加字体图标需要下面文件:src/fonts/iconfont.ttf、src/fonts/iconfont.woff、 src/css/iconfont.css;引入后使用字体图标<i class="iconfont icon-arrow-down"></i>
  2. 需要在webpack处理ttf、woff2?结尾的文件,不然浏览器识别不出来。[{test: /\.(ttf|woff2?|map4|map3|avi)$/, type: "asset/resource", generator: {filename: "static/media/[hash:8][ext][query]",}}]
  3. type: "asset/resource":相当于file-loader, 将文件转化成 Webpack 能识别的资源,其他不做处理
开发服务器&自动化
  1. 安装 npm i webpack-dev-server -D
  2. 开发服务器配置 devServer: { host: "localhost", port: 3000, open: true}
  3. 含义:启动服务器域名、服务器端口号、是否自动打开浏览器,当使用开发服务器时,所有代码都会在内存中编译打包,并不会输出到 dist 目录下。
  4. 运行的指令为npx webpack serve
处理html资源
  1. 下载html-webpack-plugin
  2. 作用: dist 目录输出一个 index.html 文件会自动引入打包生成的js等资源
  3. 使用:plugins: [new HtmlWebpackPlugin({template: path.resolve(__dirname, "public/index.html"),})],以public/index.html为模板创建文件。
  4. 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
处理js资源
  1. Webpack对js处理是有限的,只能编译js中ES 模块化语法不能编译其他语法,导致js不能在IE等浏览器运行,所以需要做一些兼容性处理,先完成Eslint检测代码格式无误后,再由Babel做代码兼容性处理。
  2. Eslint:它是用来检测 js 和 jsx 语法的工具,可以配置各项功能;需要新建配置文件.eslintrc,里面有各种rules,运行Eslint时就会以写的规则对代码进行检查。
  3. 使用:安装eslint-webpack-plugin,webpack配置:plugins: [new ESLintWebpackPlugin({ context: path.resolve(__dirname, "src")}) //指定检查文件的根目录
  4. Babel:主要将es6语法转为向后兼容的js语法,可以让它们在旧版浏览器也能用,需要新建.babelrc.
  5. Babel使用:下载插件:babel-loader @babel/core @babel/preset-env,webpack配置 [{test: /.js$/, exclude: /node_modules/, loader: "babel-loader",}]
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
 module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        include: path.resolve(__dirname, "../src"),
        loader: "babel-loader",
        options: {
          cacheDirectory: true,
          cacheCompression: false,
          plugins: [
            "@babel/plugin-transform-runtime"
          ],
        },
      }]
 },
 plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules",
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../public/index.html"),
    })
],

提升打包构建速度-HMR/Cache/Thread

Cache
  1. Cache是什么?每次打包文件都要经过Eslint Bebal处理,速度比较慢;可以缓存之前的Eslint检查的结果和Babel编译的结果,这样第二次打包速度就比较快。
  2. babel: 开启babel编译缓存{test: /.js$/, loader: "babel-loader", option:{cacheDirectory: true, }}
  3. Eslint: cache开启缓存,cacheLocation缓存目录。new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname,"../node_modules/.cache/.eslintcache")})
Include/Exclude
  1. 在开发时候使用的第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。
  2. include(包含):include: path.resolve(__dirname, "../src")
  3. exclude(排除):exclude: "node_modules"
Thread
  1. 当项目越来越庞大时,打包速度越来越慢,提升打包速度,其实就是要提升 js 的打包速度,因为其他文件都比较少,而对 js 文件处理主要就是eslint 、babel、Terser三个工具,主要是提升这三个的速度。开启多进程同时处理 js 文件。
  2. 多进程打包是开启多个进程同时干一件事,速度更快。仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。
  3. 下载thread-loader, 获取CPU的核数,启动进程的数量就是CPU的核数:const os = require("os"); const threads = os.cpus().length;
  4. loader开启多进程: {test: /\.js$/,use: [{loader: "thread-loader",options: {workers: threads}}, {loader: "babel-loader", options: {cacheDirectory: true}}]}
  5. eslint开启多进程: new ESLintWebpackPlugin({cache: true, cacheLocation:...,threads})
  6. Terser开启多进程: optimization: {minimize: true, minimizer: [new CssMinimizerPlugin(), new TerserPlugin({parallel: threads})]},生产模式会默认开启TerserPlugin,但是要进行其他配置,就要重新写了
module: {
    // 开启babel编译缓存
    rules: [
      {
        test: /\.(jsx|js)$/,
        include: path.resolve(__dirname, "../src"),
        loader: "babel-loader",
        options: {
          cacheDirectory: true,
          cacheCompression: false,
          plugins: [
             "@babel/plugin-transform-runtime" 
          ],
        },
      }]
},
plugins: [
    // ESLint开启多线程以及缓存
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules",
      cache: true,
      threads,
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:10].css",
      chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
    }),
],
optimization: {
    // 压缩的操作开启多进程
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserWebpackPlugin({parallel: threads}),
    ]
}
HotModuleReplacement
  1. HMR是什么?在开发过程中修改某一块代码,会默认把所有的模块重新打包,速度很慢。HMR可以在修改某个模块的代码时候,只打包编译某个模块,这样速度就会很快。
  2. 热模块替换就是在程序运行中,添加、删除或替换模块,无需重新加载整个页面
  3. 怎么用:在devserver里面设置hot:true就是开启了HMR功能,这个时候css经过style-loader处理就可以做到修改后不加载整个页面了。js哪个文件需要热更新需要额外写上module.hot.accept("./js/count.js")
  4. 实际开发我们会使用其他loader来解决,比如vue-loader react-hot-loader

减少代码体积-Babel/图片压缩/code split

Image Minimizer
  1. 项目如果中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。所以可以对图片进行压缩,减少图片体积
  2. 下载npm i image-minimizer-webpack-plugin imagemin -D;以及压缩各种格式图片的插件
  3. 无损压缩npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
  4. 有损压缩npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
  5. 配置 optimization: {minimize: true, minimizer: [new CssMinimizerPlugin(), new ImageMinimizerPlugin({minimizer: {...配置压缩各种格式图片的插件}}) ]}
Code Split
  1. 代码分割是什么?打包代码时会将所有 js 文件打包到一个文件中,体积太大了。所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样每次加载的资源就少,速度就更快。
  2. 种类:多入口打包;提取插件代码或重复代码到公共模块;使用import按需加载,动态导入
  3. 多入口打包:在entry配置了几个入口,输出几个 js 文件。入口文件:entry: {main: "./src/main.js",app: "./src/app.js"} ;出口文件:output: {path: path.resolve(__dirname, "./dist"), filename: "js/[name].js",}
  4. 提取插件代码或重复代码到公共模块:optimization: {splitChunks: {chunks: "all", cacheGroups: { default: {minSize: 0, minChunks: 2,priority: -20,}}}},cacheGroups 修改配置组,配置哪些模块要打包到一个组。
  5. 使用import按需加载,动态导入:import(/* webpackChunkName: "math" */ "./js/math.js").then(({ count }) => {...})
Core-js
  1. 使用babel对js进行兼容性处理,使用@babel/preset-env智能预设处理兼容性问题,它可以对ES6的扩展运算符、箭头函数进行处理,但是版本更高的promise对象、async函数没办法处理。
  2. core-js是专门来做es6及以上的api polyfill,叫做垫片/补丁。就是用社区上提供的一段代码,可以在不兼容某些新特性的浏览器上,使用该新特性。
  3. 如何使用? 在babel.config.js文件,直接 import "@babel-polyfill"会过大,只使用一部分功能可以按需引用。
module.exports = {
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      // { useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
      { useBuiltIns: "usage", corejs: 3 },
    ],
  ],
};
Tree Shaking
  1. Webpack开发模式已经默认开启了这个功能,无需其他配置。
  2. 开发时引用第三方工具函数库或组件库,如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能;
  3. Tree Shaking移除 JavaScript 中的没有使用上的代码。它是基于es6模块静态编译思想,在编译时候确定模块的依赖关系、输入输出的变量。在编译阶段利用ES Module判断哪些模块已经加载,哪些未被使用进而删除对应的代码。

链接

尚硅谷Webpack5入门到原理

React Native 性能优化指南

作者 野生好人
2026年3月6日 09:49

React Native 性能优化指南(2024-2026)

React Native 在 2024 年底至 2026 年间经历了重大的架构演进(New Architecture),结合 Hermes 引擎的默认化,使其性能接近原生应用。以下是基于最新生态和官方更新的系统性优化指南。

1. 核心架构与引擎(基础升级)

目的:解决原有桥接(Bridge)导致的性能瓶颈。 操作

  • 迁移 New Architecture:将项目迁移到最新架构(默认从 RN 0.76 开始),启用 Fabric 渲染引擎和 TurboModules。这是提升渲染性能的根本措施[[1]][[2]]。
  • 启用 Hermes 引擎:强制开启 Hermes AOT 编译(预编译),并开启 hermes bytecode 功能,显著减少启动时间和内存占用[[3]][[4]]。
  • 避免不必要的桥接:尽量使用 JSI(JavaScript Interface)或 TurboModules 替代传统的 NativeModules,降低 JavaScript 与原生的通信开销[[5]]。

2. 渲染层优化(防止卡顿)

目的:降低 UI 线程压力,避免掉帧。 操作

  • 列表渲染:对于长列表,优先使用 FlashList 替代 FlatList,因为它采用了更高效的布局算法;若使用 FlatList,务必设置 keyExtractorremoveClippedSubviewswindowSize 参数[[6]]。
  • 避免重复渲染:使用 React.memo 包裹函数组件,使用 useMemo 缓存计算结果,使用 useCallback 缓存函数引用,防止子组件不必要的重新渲染[[7]][[8]]。
  • 图片优化:使用 react-native-fast-image 替代 Image 组件,开启磁盘缓存、预取图片并使用占位图,避免加载高分辨率图片导致的卡顿[[9]]。
  • 动画流畅:避免使用 Animated 的 JS driver。改用 react-native-reanimated 3,将动画逻辑迁移至 UI 线程(C++层),确保动画的 60fps 流畅性[[10]]。

3. JavaScript 线程优化(代码层)

目的:减少 JS 主线程阻塞。 操作

  • 性能监控:使用 Flipper 的 React DevTools 插件或 react-native-performance 库进行帧率监控,定位具体卡顿点[[11]][[12]]。
  • 避免大对象创建:在渲染循环中避免创建大对象或执行大量计算。将复杂逻辑提取到 Web Worker 中(使用 react-native-workers)或通过 TurboModules 转为原生执行[[13]]。
  • 代码分割:对于大型页面,使用动态 import() 进行懒加载,减小单个 Bundle 大小,加快加载速度[[14]]。

4. 内存管理与资源释放

目的:防止内存泄漏导致的崩溃。 操作

  • 及时释放资源:在组件的 useEffect 返回函数或 componentWillUnmount 中,移除所有事件监听、计时器和网络请求,防止回调引用导致的内存泄漏[[15]]。
  • 图片资源:使用 FastImageclearDiskCacheclearMemoryCache 方法定期清理缓存,防止 OOM(内存溢出)[[16]]。
  • 键盘弹出:在使用 KeyboardAvoidingView 时,尽量设置 behavior="padding" 并控制键盘监听的数量,键盘弹出是常见的卡顿来源[[17]]。

5. 包体积与安全性优化

目的:减小 App 下载体积,提高安全性。 操作

  • ProGuard/R8:在 Android 项目中启用 ProGuard(enableProguardInReleaseBuilds),移除未使用的 Java 类和资源[[18]]。
  • 移除 Debug 代码:确保发布版(Release)中移除所有 console.logdebugger 语句,关闭开发者菜单[[19]]。
  • 依赖审计:定期使用 npm audit 检查依赖库的安全性和冗余程度,删除未使用的第三方库[[20]]。

6. 迁移与版本管理

目的:确保新架构兼容性。 操作

  • 版本锁定:使用 Yarn 的 resolutions 锁定 React Native 关键依赖版本,防止因子依赖升级导致的崩溃[[21]]。
  • 分支管理:为迁移 New Architecture 创建专门的 new-arch 分支,逐步替换旧模块,避免一次性迁移导致的全局错误[[22]]。

参考文献

  1. React Native 性能优化实战指南(2026最新版)[[23]]
  2. React Native 新架构概念解析[[24]]
  3. React Native 性能优化 Checklist[[25]]
  4. Callstack 社区 2024 性能更新与指南[[26]]
  5. 优化 React Native 性能:技巧与最佳实践(GCC Marketing)[[27]]
❌
❌