普通视图
应对存储芯片涨价,一家PC高管密集拜访三星、SK海力士敲定供货
港交所:2025年香港新股市场融资额位居全球第一
恒指收涨0.43%,恒生科技指数涨0.87%
中汽协:1-11月销量排名前十SUV生产企业共销售958.3万辆
中汽协:1-11月销量排名前十轿车生产企业共销售782.6万辆
意大利监管机构对苹果罚款上亿美元
比亚迪回应“技术研发人员涨薪”:情况属实
全国首个开放式机器人租赁平台“擎天租”在上海发布
你每天都在用的 JSON.stringify ,V8 给它开了“加速通道”
V8 如何把 JSON.stringify 性能提升 2 倍
JSON.stringify 应该是 JavaScript 里用得最多的函数之一了。
API 响应要序列化,日志要格式化,数据要存 localStorage,调试要打印对象……几乎每个项目都离不开它。
但说实话,用的时候很少会想"这玩意儿快不快"。反正就是调一下,能用就行。
V8 团队显然不这么想。V8 是 Chrome 和 Node.js 背后的 JavaScript 引擎,你写的每一次 JSON.stringify,最后都要靠它来跑。2025 年 8 月,他们发了篇博客,讲了怎么把 JSON.stringify 的性能提升到原来的 2 倍以上。
这篇文章拆解一下他们做了什么。
读者导航:不懂 V8 也能看
先记住三句话就够了:
- 绝大多数优化都在“走捷径”:先判断输入是不是“简单、可预测”的对象,是的话走更快的路径。
- 很多名词听着硬,其实都在做同一件事:减少检查、减少函数调用、让 CPU 一次干更多活、减少内存搬运。
-
你能做的配合也很简单:少用会触发副作用的写法(getter、
toJSON、格式化参数),保持数据对象“干净”。
下面遇到生词可以先跳过,看完“对开发者的启示”再回头补。
优化的前提:无副作用检测
JSON.stringify 慢在哪?
一个重要原因是它要处理各种边界情况:对象可能有 toJSON 方法,属性可能是 getter,可能有循环引用……这些都可能产生副作用,导致序列化结果不可预测。
V8 的第一步优化是:检测对象是否"干净"。
如果能确定序列化过程不会触发任何副作用,就可以走一条快速路径,跳过大量的安全检查。
"if we can guarantee that serializing an object will not trigger any side effects, we can use a much faster, specialized implementation."
这条快速路径用迭代替代了递归,好处有两个:
- 不用担心栈溢出(深层嵌套对象)
- 减少函数调用开销
字符串处理:双版本编译
JavaScript 字符串有两种内部表示:单字节(Latin-1)和双字节(UTF-16)。
以前 V8 用统一的方式处理,现在编译了两个特化版本的序列化器,分别针对这两种编码优化。可以简单理解成:如果字符串全是英文数字,就走“单字节快车道”;如果包含中文表情等,就走“UTF-16 车道”。
遇到混合编码的情况(比如一个对象里既有纯 ASCII 字符串,又有中文),会在执行时动态切换。
这种"按需特化"的思路在编译器优化里很常见,但用在 JSON 序列化上还是挺有意思的。
SIMD 加速字符扫描
序列化字符串时,需要扫描哪些字符需要转义(比如 \n、\t、")。
V8 用了两种硬件加速策略:
- SIMD 指令:对于较长的字符串,一次处理多个字符(你可以理解成“把 16 个字节打包一起扫一遍”)
- SWAR 技术:对于较短的字符串,用位运算在普通寄存器上并行处理(SIMD 的“轻量版”)
SWAR(SIMD Within A Register)是个挺老的技术,思路是把一个 64 位寄存器当成 8 个 8 位的"小寄存器"用,通过位运算实现并行。
举个例子,判断一个字节是否需要转义,可以这样:
// 伪代码示意
// 需要转义的字符:< 0x20 或 == 0x22(") 或 == 0x5C(\)
function needsEscape(byte) {
return byte < 0x20 || byte === 0x22 || byte === 0x5C;
}
用 SWAR 可以一次判断 8 个字节,只要用合适的掩码和位运算。
Hidden Class 标记
V8 内部用 Hidden Class(隐藏类)来优化对象属性访问。你可以把它理解成“对象结构的身份证”:同一类对象(同样的字段、同样的顺序)会复用同一个结构描述。
这次优化加了一个新标记:fast-json-iterable。
当一个对象的属性满足特定条件时,就给它打上这个标记。下次序列化同类对象时,直接跳过验证检查。
这就是典型的"用空间换时间"——在对象上多存一个标记,换来后续操作的加速。
数字转字符串:换算法
把数字转成字符串也是序列化的一部分。
V8 以前用 Grisu3 算法,现在换成了 Dragonbox。
你不需要理解算法细节,只要知道:Dragonbox 能更快、更稳定地把浮点数转成最短且精确的十进制表示。这个改动不只影响 JSON 序列化,所有 Number.toString() 都能受益。
内存管理:分段缓冲区
以前序列化大对象时,V8 用一块连续内存作为缓冲区。对象越大,缓冲区就要不断扩容,每次扩容都要重新分配和复制。
新实现用分段缓冲区(segmented buffer),多个小块链起来用,避免了昂贵的重分配。直觉上就是:不强求“一次申请一大块”,而是“够用就多挂一块”。
这个思路和 Linux 内核的 sk_buff 链表类似——不追求内存连续,换来分配效率。
快速路径的适用条件
不是所有 JSON.stringify 调用都能走快速路径。需要满足:
-
不传 replacer 和 space 参数
// 可以走快速路径 JSON.stringify(obj); // 不行 JSON.stringify(obj, null, 2); JSON.stringify(obj, ['name', 'age']);这里的
replacer/space分别是“过滤/改写字段的回调或白名单”和“为了好看而加的缩进”。它们会让序列化过程更复杂,所以很难走最激进的优化路径。 -
纯数据对象
- 没有
toJSON方法 - 没有 getter
- 没有 Symbol 属性
- 没有
-
字符串键
- 所有属性名都是字符串(不是数字下标)
-
简单字符串值
- 字符串值本身没有特殊情况
实际项目里,大部分序列化场景都满足这些条件。API 返回的纯 JSON 数据、配置对象、日志数据……基本都能用上。
什么时候能用?
Chrome 138 / V8 13.8 开始可用。
Node.js 的话,需要等对应的 V8 版本合入。目前最新的 Node.js 22 用的是 V8 12.x,还得等一等。
对开发者的启示
虽然优化是 V8 团队做的,但有几点可以参考:
1. 保持对象"干净"
避免在需要序列化的对象上加 getter 或 toJSON 方法。如果必须用,考虑在序列化前转换成纯数据对象。
// 不太好
class User {
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
// 更好
const user = {
firstName: 'John',
lastName: 'Doe',
fullName: 'John Doe' // 直接计算好
};
2. 大量序列化时考虑结构一致性
V8 的 Hidden Class 优化依赖对象结构一致。如果你要序列化大量对象,保持它们的属性顺序和类型一致。
// 好
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 }
];
// 不太好(age 类型不一致)
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: '30' }, // 字符串
{ name: 'Charlie', age: null }
];
3. 避免无意义的格式化
开发环境用 JSON.stringify(obj, null, 2) 看着舒服,但这会跳过快速路径。生产环境记得去掉。
// 开发环境
console.log(JSON.stringify(data, null, 2));
// 生产环境
console.log(JSON.stringify(data));
总结
- 分析热点:找到可以优化的场景(无副作用对象)
- 特化路径:针对常见情况走快速路径,边界情况走通用路径
- 硬件加速:用 SIMD 和 SWAR 提升字符处理速度
- 利用已有信息:通过 Hidden Class 标记避免重复验证
- 改进算法:Dragonbox 替代 Grisu3
- 优化内存:分段缓冲区避免重分配
这些技术单拿出来都不新鲜,但组合起来能把一个已经很成熟的 API 性能翻倍,还是挺厉害的。
对我们写代码的人来说,最大的收获可能是:写"干净"的代码不只是为了可读性,有时候还能让引擎更好地优化。
延伸阅读
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB
Electron无边框窗口如何拖拽以及最大化和还原窗口
什么是无边框窗口
设置了frame: false,新建的窗口就没有边框了!
mainWindow = new BrowserWindow({
width: 800,
height: 600,
frame: false, // 无边框核心配置
webPreferences: {
nodeIntegration: true, // 启用 Node 集成,允许渲染进程调用 Electron API
contextIsolation: false // 关闭上下文隔离,简化调用
}
});
无边框窗口如何拖拽
Electron无边框窗口默认无法拖拽,需在 HTML 中给可拖拽区域添加 CSS 样式 -webkit-app-region: drag;(注意:按钮等可点击元素需设置 webkit-app-region: no-drag; 避免无法点击)。
// 可拖拽部分
.header {
-webkit-app-region: drag;
}
// 不需要拖拽部分
.main {
-webkit-app-region: no-drag;
}
如何最大化窗口和还原
Electron提供了窗口的最大化和还原窗口的api
// 最大化窗口
mainWindow.maximize()
// 还原窗口
mainWindow.unmaximize();
但是!在无边框窗口中使用mainWindow.maximize()最大化窗口并不能被还原,此时就需要其他方法来实现此功能。
首先判断是否处于全屏状态
// 兼容所有浏览器的全屏状态判断
function isDocumentFullScreen() {
return !!document.fullscreenElement ||
!!document.webkitFullscreenElement || // 对应 webkitIsFullScreen 的元素版
!!document.mozFullScreenElement || // 对应 mozFullScreen 的元素版
!!document.msFullscreenElement;
}
你在使用 TypeScript 时,调用 document.webkitFullscreenElement、document.mozFullScreenElement 等厂商前缀全屏 API 出现类型错误,这是因为 TypeScript 的内置 DOM 类型定义中仅包含标准 API(如 document.fullscreenElement),未包含这些非标准的厂商前缀属性,以下是几种完整的解决方案,按推荐优先级排序:
一、方案 1:类型断言(Type Assertion)—— 快速解决单个属性报错
这是最简洁的临时解决方案,通过类型断言告诉 TypeScript 「该属性存在于 document 上」,忽略类型检查报错。
实现代码
// 兼容所有浏览器的全屏状态判断(TS 兼容写法)
function isDocumentFullScreen(): boolean {
// 对 document 进行类型断言,指定为包含厂商前缀属性的任意类型
const doc = document as any;
return !!doc.fullscreenElement ||
!!doc.webkitFullscreenElement ||
!!doc.mozFullScreenElement ||
!!doc.msFullscreenElement;
}
// 或直接对单个属性进行断言(更精准)
function isDocumentFullScreen精准版(): boolean {
return !!document.fullscreenElement ||
!!(document as { webkitFullscreenElement?: Element | null }).webkitFullscreenElement ||
!!(document as { mozFullScreenElement?: Element | null }).mozFullScreenElement ||
!!(document as { msFullscreenElement?: Element | null }).msFullscreenElement;
}
特点
- 优点:快速便捷,无需额外配置,适合简单场景或临时调试。
- 缺点:缺乏类型提示,若属性名拼写错误(如把
webkitFullscreenElement写成webkitFullScreenElement),运行时才会暴露问题。
二、方案 2:扩展全局 DOM 类型(推荐)—— 长期优雅解决
通过 TypeScript 的「全局类型扩展」功能,为 Document 接口补充厂商前缀属性的类型定义,既解决报错,又能获得类型提示,是长期项目的最优解。
步骤 1:创建类型声明文件(如 global.d.ts)
在项目根目录或 src 目录下创建 .d.ts 后缀的类型声明文件(TS 会自动识别该类型文件,无需手动引入):
// global.d.ts
declare global {
interface Document {
// 补充 WebKit/Blink 内核厂商前缀属性
webkitFullscreenElement?: Element | null;
// 补充 Gecko 内核厂商前缀属性(注意:S 大写,与标准 API 有差异)
mozFullScreenElement?: Element | null;
// 补充 Trident 内核厂商前缀属性
msFullscreenElement?: Element | null;
// 可选:若需使用旧版布尔值属性,也可补充对应类型
webkitIsFullScreen?: boolean;
mozFullScreen?: boolean;
}
}
// 必须导出空对象,标识这是一个模块
export {};
步骤 2:配置 tsconfig.json(确保类型文件被识别)
确保 tsconfig.json 中包含该类型文件的路径(默认情况下,"include" 会覆盖 src 下所有 .ts/.d.ts 文件,若已配置可跳过):
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./src/types"] // 若类型文件放在 src/types 下,需配置此路径
},
"include": ["src/**/*", "global.d.ts"] // 包含全局类型声明文件
}
步骤 3:正常使用代码(无报错且有类型提示)
// 此时 TS 不会报错,且能自动提示对应属性
function isDocumentFullScreen(): boolean {
return !!document.fullscreenElement ||
!!document.webkitFullscreenElement ||
!!document.mozFullScreenElement ||
!!document.msFullscreenElement;
}
特点
- 优点:类型安全、有代码提示,一劳永逸,适合长期维护的项目。
- 缺点:需要额外创建类型文件,初次配置略繁琐。
补充说明
- 厂商前缀 API 已逐步被废弃,若项目无需兼容老旧浏览器,推荐直接使用标准 API
document.fullscreenElement,无需额外处理类型问题。 - 若使用方案 2 后仍报错,可重启 TS 语言服务(VS Code 中可按
Ctrl+Shift+P,输入「TypeScript: Restart TS Server」)。 - 类型声明文件中,属性添加
?表示可选属性(因为并非所有浏览器都存在这些属性),符合实际兼容场景。
进入全屏
if (!isFull) { // 进入全屏
let element = document.documentElement;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
}
}
-
逻辑:先获取页面根元素
document.documentElement(即<html>标签,通常让整个页面进入全屏),再优先调用标准 API,降级调用厂商前缀方法:-
element.requestFullscreen():标准进入全屏方法(需指定 “要全屏的元素”)。 -
element.msRequestFullscreen():IE / 旧版 Edge 的进入全屏方法。 -
element.mozRequestFullScreen():Firefox 的进入全屏方法(注意命名是RequestFullScreen)。 -
element.webkitRequestFullscreen():Chrome/Safari 的进入全屏方法。
-
退出全屏
if (isFull) {// 退出全屏
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
-
逻辑:优先调用标准 API,若浏览器不支持,则降级调用对应厂商前缀的 “退出全屏” 方法:
-
document.exitFullscreen():标准退出全屏方法。 -
document.msExitFullscreen():IE / 旧版 Edge 的退出全屏方法。 -
document.mozCancelFullScreen():Firefox 的退出全屏方法(注意命名是CancelFullScreen)。 -
document.webkitExitFullscreen():Chrome/Safari 的退出全屏方法。
-
对外贸易法修订草案二审稿进一步支持发展数字贸易、绿色贸易
2024年度审计整改报告发布,年度审计整改问题金额1.04万亿元
Java 异常处理:3 个 “避坑神操作”,告别崩溃式报错
做 Java 开发谁没踩过异常的坑?比如空指针直接让程序 “原地去世”,try-catch 裹成 “千层饼”,报错信息含糊到 debug 半小时找不到问题。今天分享 3 个最常用的异常处理技巧,代码简洁还实用,新手也能轻松拿捏~
1. 空指针防护:别再写一堆 if 判空!
日常开发中,空指针是最常见的异常。推荐用Objects.requireNonNullElse(Java 9+)或Optional类,一行搞定空值处理:
import java.util.Objects;
import java.util.Optional;
public class ExceptionDemo {
public static void main(String[] args) {
String userName = null; // 模拟可能为空的参数
Integer age = null;
// 技巧1:Objects工具类(直接指定默认值)
String safeName = Objects.requireNonNullElse(userName, "匿名用户");
System.out.println("用户名:" + safeName); // 输出:匿名用户
// 技巧2:Optional类(更灵活的空值处理)
Integer safeAge = Optional.ofNullable(age)
.orElse(18); // 为空则返回默认值18
System.out.println("年龄:" + safeAge); // 输出:18
// 进阶:为空时抛自定义异常
Optional.ofNullable(userName)
.orElseThrow(() -> new IllegalArgumentException("用户名不能为空!"));
}
}
2. 多异常处理:别写 N 个 catch 块!
多个异常只需统一处理时,用 “|” 合并异常类型,代码瞬间清爽:
// 技巧3:合并异常处理(避免重复代码)
public static void handleMultiException(String str) {
try {
Integer num = Integer.parseInt(str); // 可能抛NumberFormatException
System.out.println("转换结果:" + num);
} catch (NumberFormatException | NullPointerException e) {
// 一个catch处理两种异常
System.out.println("错误:输入参数无效!原因:" + e.getMessage());
}
}
// 调用测试
public static void main(String[] args) {
handleMultiException("abc"); // 抛NumberFormatException
handleMultiException(null); // 抛NullPointerException
}
二、关键注意事项
-
别用
e.printStackTrace()!建议用日志框架(如 SLF4J)打印,方便定位问题; -
自定义异常时,要包含详细的错误信息,别只抛 “发生异常” 这种无用提示;
-
不要滥用 try-catch,比如不需要处理的运行时异常(如 IndexOutOfBoundsException),不如让程序快速失败,便于排查问题。