尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

三、浏览器引擎与垃圾回收机制

三、浏览器引擎与垃圾回收机制 一、什么是浏览器引擎简单来说浏览器引擎就是负责把“代码”变成“网页画面”的幕后翻译官和施工队。在专业领域我们常说的“浏览器内核/引擎”其实是由两个核心部分组成的渲染引擎Rendering Engine—— 负责“装修和画画”它的工作接收 HTML 和 CSS 代码计算出每个元素的大小、颜色、位置最后把它们“画”在屏幕上。实际开发例子你写了一个按钮button点击我/button渲染引擎会读取它的 CSS把它变成屏幕上一个有圆角、有阴影、写着“点击我”的立体方块。JavaScript 引擎—— 负责“逻辑和互动”它的工作读取并执行 JavaScript 代码处理用户的操作比如点击、数据请求等。实际开发例子当用户真的点击了那个按钮JS 引擎会立刻响应弹出一个提示框或者向服务器发送一个请求。前端开发避坑指南如果你用 JS 频繁地修改页面的宽高渲染引擎就需要不断地重新计算和画画也就是常说的重排和重绘。这就像装修工人刚刷完墙你又让他砸掉重新量尺寸非常耗费体力导致页面卡顿。所以我们要尽量减少这种操作。二、主流的浏览器引擎有哪些因为各大浏览器厂商的“后厨”配方不同所以做出来的网页效果偶尔也会有细微差别。目前市面上主流的引擎有以下几种1. Blink 引擎目前最主流的“霸主”谁在用Chrome、Edge、Opera 以及国内绝大多数浏览器如 360极速、QQ浏览器等。特点由 Google 主导开发。它的特点是性能极强、更新极快。实际开发体验作为前端我们平时写代码基本都是在 Chrome 里调试因为它是目前的行业事实标准绝大多数新出的前端技术如最新的 CSS 特性它都支持得最好。2. WebKit 引擎苹果生态的“守门员”谁在用苹果 Safari 浏览器。注意iOS 系统上的所有浏览器包括 Chrome 的 iOS 版底层用的都是 WebKit。特点非常注重省电和流畅度但在支持某些最新的 Web 标准时比较保守。实际开发体验这是前端最头疼的“兼容性重灾区”。比如你想在网页里让一个视频自动播放Chrome 里可能好好的但在 Safari 里就会被拦截苹果出于安全和省电的考虑强制要求必须有用户的交互动作才能播放视频。3. Gecko 引擎坚守标准的“老派贵族”谁在用Mozilla Firefox火狐浏览器。特点非常严谨地遵循 W3C 的 Web 国际标准不轻易搞“私有特性”。实际开发体验如果你写的代码在 Chrome 和 Firefox 里表现不一样那大概率是你的代码写得不符合标准或者 Chrome 做了额外的“宽容处理”。补充三大引擎对比速查表引擎代表浏览器核心优势常见兼容性痛点BlinkChrome、Edge、Opera性能极强标准支持领先某些私有前缀可能被滥用WebKitSafari省电、流畅安全策略严格视频自动播放、部分 CSS 特性需特殊处理GeckoFirefox严格遵循 W3C 标准对代码规范性要求高异常行为多半是开发者代码问题三、最佳实践1. 渲染战场让“画画”更轻松渲染引擎最怕的就是你让它反复“量尺寸”和“重新刷墙”。避免“强制同步布局”Layout Thrashing这是什么 就像装修工人刚量好尺寸准备刷漆你突然问“哎这面墙现在多高” 他只能放下刷子重新拿尺子量量完再继续刷。如果你在一秒钟内问100次他就得量100次活肯定干不完。实际开发例子// ❌ 错误示范在循环里“读”完立刻“写”for(leti0;i100;i){constheightelement.offsetHeight;// 读强制引擎立刻计算布局element.style.heightheight10px;// 写布局失效下次循环又要重新计算}✅最佳实践批量读批量写。// ✅ 正确示范先读完所有要读的值constheights[];for(leti0;i100;i){heights.push(element.offsetHeight);// 只读}// 再统一修改for(leti0;i100;i){element.style.heightheights[i]10px;// 只写}动画优先用 transform 和 opacity这是什么 渲染引擎会把页面分成很多“图层”。修改transform如位移、旋转或opacity透明度引擎可以直接交给 GPU 去合成完全跳过耗时的“布局”和“绘制”阶段。实际开发例子❌ 想让一个卡片从左滑到右修改left或margin-left。这会触发重排非常卡。✅ 想让一个卡片从左滑到右使用transform: translateX(100px)。这会触发合成丝般顺滑。补充技巧如果某个元素即将频繁发生动画可以提前声明will-change: transform让浏览器提前创建独立的图层进一步优化性能。2. JS 引擎战场让“逻辑”跑得更快JS 引擎比如 Chrome 的 V8是个“偏科生”它特别擅长优化结构稳定、重复执行的代码。保持对象结构一致这是什么 V8 会为结构相同的对象创建一个“隐藏类”Hidden Class并建立属性访问的快速通道内联缓存。如果你动态增删属性或者创建对象时属性顺序不一致就会破坏这个优化让引擎退回“慢速模式”。实际开发例子// ❌ 错误示范动态添加属性破坏隐藏类functioncreateUser(name){constuser{name:name};if(someCondition){user.age18;// 动态添加引擎懵了}returnuser;}✅最佳实践在构造函数里一次性定义所有属性。// ✅ 正确示范结构清晰引擎狂喜functioncreateUser(name,age){return{name:name,age:age||null// 即使没有也占个坑保持结构一致};}避免长任务学会“拆分”这是什么 JS 是单线程的一个超过 50ms 的任务就会阻塞主线程导致用户点击没反应、动画卡顿。实际开发例子❌ 一次性处理一个包含 10 万条数据的数组页面直接卡死。✅最佳实践拆分微任务用setTimeout或requestIdleCallback把大任务切成小块在浏览器空闲时执行。交给 Web Worker如果是纯计算不涉及 DOM直接丢给 Worker 线程让主线程专心处理渲染和用户交互。3. 资源与网络战场让“加载”更迅速引擎再快也得等“食材”资源送到。图片优化是重中之重✅最佳实践格式优先用 WebP 或 AVIF体积比 JPEG/PNG 小得多。懒加载给非首屏图片加上loadinglazy用户滚动到附近再加载。响应式用picture或srcset让手机加载小图电脑加载大图。善用缓存✅最佳实践静态资源配置Cache-Control: max-age31536000让浏览器缓存一年。文件名带 Hash内容一变文件名就变完美解决缓存更新问题。Service Worker对于核心页面或离线功能用 SW 做缓存策略实现秒开和离线可用。关键资源预加载✅最佳实践!-- 告诉浏览器这个 CSS 很重要赶紧下载 --linkrelpreloadhrefcritical.cssasstyle!-- 告诉浏览器下一页可能用到这个 JS空闲时下载 --linkrelprefetchhrefnext-page.js四、垃圾回收在前端开发中我们不需要像 C 程序员那样手动去申请和释放内存。JavaScript 引擎比如 Chrome 的 V8自带了一个自动清洁工这就是垃圾回收机制Garbage Collection简称 GC。它的工作就两件事找出不再使用的内存找垃圾然后释放它们清垃圾。1. 怎么判断谁是垃圾核心算法浏览器主要用两种算法来判定垃圾你可以把它们理解为两种不同的找垃圾逻辑标记-清除Mark-and-Sweep—— 现代浏览器的主流通俗解释清洁工拿着手电筒从根部比如全局对象window、当前正在执行的函数出发顺着引用关系挨个找。凡是能被找到的打上存活标记找不到的就是垃圾直接清理掉。优点能完美解决循环引用的问题。实际开发例子letobj1{name:obj1};letobj2{name:obj2};obj1.refobj2;obj2.refobj1;// 它们互相引用obj1null;obj2null;// 即使它们互相引用但从 window 已经找不到它们了会被当成垃圾清理掉引用计数Reference Counting—— 逐渐被淘汰的老方法通俗解释给每个对象发个点赞牌。有人引用它点赞数 1引用取消点赞数 -1。当点赞数变成 0 时说明没人用它了当成垃圾清理。致命缺陷循环引用如果 A 引用了 BB 又引用了 A即使整个页面都不再需要它们了它们的点赞数也永远是 1永远无法被回收这就导致了内存泄漏。补充说明现代浏览器包括 IE9已经全面采用标记-清除算法因此循环引用在绝大多数场景下不再导致内存泄漏但了解引用计数的缺陷有助于理解 GC 的演进历史。两种算法对比速查表算法判断标准循环引用当前状态标记-清除从根对象出发可达即存活✅ 完美解决主流算法引用计数引用次数为 0 即垃圾❌ 无法处理已淘汰2. 现代浏览器的清洁策略分代回收为了提高效率V8 引擎把内存分成了两个区域实行因地制宜的回收策略这叫分代回收Generational Collection新生代Young Generation—— 朝生夕死特点存放刚创建的新对象绝大多数对象很快就会死掉不再被使用。空间很小通常 1~8MB。回收算法Scavenge / 复制算法把内存一分为二From 区和 To 区。新对象放在 From 区。垃圾回收时把还活着的对象全部复制到 To 区然后把 From 区一次性清空。晋升机制如果一个对象在新生代里经历了几次回收还活着说明它是老寿星会被提拔晋升到老生代。老生代Old Generation—— 长期存活特点存放生命周期长的对象比如全局变量、DOM 节点、闭包引用的对象。空间大。回收算法标记-清除 / 标记-整理因为对象多、空间大不能像新生代那样复制。引擎会先标记出存活的对象然后把它们往内存的一端紧凑移动标记-整理最后把边界外的空间一次性释放。这样还能避免内存产生碎片。分代回收流程示意首次分配存活多次存活死亡新创建的对象分代判断新生代From 区Scavenge 回收复制到 To 区晋升到老生代直接清除标记-清除 / 标记-整理释放内存3. 为什么有时候页面会卡顿Stop-The-World这是新手必须知道的核心概念在传统的垃圾回收过程中JavaScript 引擎必须暂停当前正在执行的业务代码这被称为**“全停顿Stop-The-WorldSTW”**。卡顿原因如果你在一个循环里疯狂创建几万个临时对象新生代的内存瞬间爆满就会频繁触发 GC。GC 一工作主线程就暂停用户就会感觉到页面掉帧、鼠标点不动。现代优化为了减少卡顿现代浏览器引入了增量标记和并发回收。把原本需要一次性做完的标记工作拆分成无数个小步骤穿插在 JS 代码执行的间隙去做或者交给后台线程去做让你几乎感觉不到停顿。补充说明Chrome 还引入了惰性清除Lazy Sweeping将清除阶段也延迟到空闲时段执行进一步降低 STW 的影响。五、怎么避免内存泄漏1. 事件监听的成对出现原则这是新手最容易犯的错误。只要你在代码里写了addEventListener就必须在对应的地方写removeEventListener。实际开发例子在 Vue 或 React 中如果你在组件挂载mounted/useEffect时绑定了window的滚动事件那么在组件卸载unmounted/useEffect的清理函数时必须将其移除。进阶技巧如果事件只需要触发一次可以使用{ once: true }参数浏览器会自动帮你清理省心又安全。// ✅ 使用 { once: true } 自动清理element.addEventListener(click,handleClick,{once:true});2. 定时器的下班打卡原则setInterval和setTimeout就像是你雇来的员工如果项目结束了你不让他们下班他们就会一直占用你的资源。实际开发例子在组件中启动了一个轮询请求的定时器一定要把返回的定时器 ID 保存下来并在组件销毁时调用clearInterval或clearTimeout将其销毁。// ✅ 正确示范组件销毁时清理定时器useEffect((){consttimerIdsetInterval((){fetchData();},5000);return(){clearInterval(timerId);// 清理函数中销毁定时器};},[]);3. 告别全局变量拥抱模块化全局变量会一直存在于内存中直到页面被关闭。实际开发例子尽量避免使用var声明变量容易意外挂载到window上而是使用let和const。采用 ES6 的模块化import/export来组织代码把变量和函数封装在模块内部避免污染全局作用域。4. 谨慎对待闭包中的大胖子闭包本身不是坏事但如果闭包意外持有了巨大的对象比如几万条数据的表格、巨大的 Base64 图片就会导致这些大对象无法被垃圾回收。实际开发例子如果在定时器或回调函数中需要用到组件的状态state 或 props不要直接把整个对象传进去而是只提取你需要的那几个字段。// ❌ 错误闭包持有整个大对象constbigData{/* 几万条数据 */};setInterval((){console.log(bigData.summary);// bigData 永远无法被回收},1000);// ✅ 正确只提取需要的字段constsummarybigData.summary;setInterval((){console.log(summary);// 只持有轻量数据},1000);5. 善用 WeakMap 和 WeakSet这是前端内存管理中的神器。普通的Map或Array会强引用里面的对象导致它们无法被回收。实际开发例子如果你想给某些 DOM 节点绑定一些额外的数据又不想在 DOM 节点被移除时手动去清理这些数据就可以使用WeakMap。当 DOM 节点被浏览器回收时WeakMap中对应的键值对会自动消失完全不需要你操心。❌ 反面教材使用普通 Map 导致的内存泄漏假设我们在开发一个网页页面上有很多商品卡片DOM 节点。我们想记录一下每个卡片被用户点击的次数。// 1. 创建一个普通 Map 来存储数据constclickCountMapnewMap();// 2. 获取页面上的一个卡片节点并给它绑定点击次数constcardNodedocument.getElementById(product-card);clickCountMap.set(cardNode,1);// 3. 业务逻辑后来这个商品下架了我们把卡片从页面上移除了document.body.removeChild(cardNode);// ⚠️ 灾难发生虽然卡片cardNode已经从页面上消失了// 但是 clickCountMap 依然死死地拽着它// 因为 Map 是强引用垃圾回收器看到 Map 还引用着 cardNode// 就绝对不会回收它。这就导致了【内存泄漏】// 必须手动清理一旦忘记就会泄漏clickCountMap.delete(cardNode);✅ 正面示范使用 WeakMap 自动断舍离现在我们把Map换成WeakMap看看会发生什么神奇的事情// 1. 创建一个 WeakMapconstclickCountWeakMapnewWeakMap();// 2. 获取卡片节点并绑定数据letcardNodedocument.getElementById(product-card);clickCountWeakMap.set(cardNode,1);// 3. 业务逻辑商品下架卡片从页面上移除document.body.removeChild(cardNode);// 4. 断开我们手里的变量引用cardNodenull;// 见证奇迹的时刻// 当你执行完 cardNode null 之后页面上已经没有这个节点了// 你的代码里也没有任何地方再引用它了。// 此时垃圾回收器会毫不留情地把这个节点清理掉。// 神奇的地方在于一旦节点被清理WeakMap 里对应的那条记录// 节点: 1也会自动消失你完全不需要写任何 delete 代码。WeakMap vs Map 对比速查表特性MapWeakMap键的类型任意类型只能是对象引用类型强引用弱引用可枚举✅ 可遍历❌ 不可遍历垃圾回收键存在即无法回收键对象被回收后自动清除适用场景通用键值存储DOM 关联数据、私有属性六、WeakMap 和 WeakSet1. 核心区别Map vs Set首先我们要明确它们各自的基础职责Map字典存的是键值对Key-Value。比如{ 张三: 18岁 }。Set集合存的是不重复的值。比如[张三, 李四]。所以WeakMap用来给对象绑定额外的数据。WeakSet用来记录某个对象是否出现过做标记。Map 与 Set 家族速查表数据结构存储形式键的类型引用类型可遍历自动清理Map键值对任意类型强引用✅❌WeakMap键值对只能是对象弱引用❌✅Set不重复的值—强引用✅❌WeakSet不重复的值只能是对象弱引用❌✅2. 它们为什么叫 “Weak”弱这是新手最容易困惑的地方。无论是 WeakMap 还是 WeakSet它们都有一个共同的铁律键Key必须是对象不能是字符串、数字等基础类型。弱引用Weak它们对对象的引用是弱的。意思是它们不会阻止垃圾回收。只要这个对象在外部没有任何人引用了垃圾回收器就会把它清理掉。一旦对象被清理它在 WeakMap 或 WeakSet 里的记录也会自动消失。⚠️ 代价是什么因为记录会随时自动消失所以它们不能遍历没有forEach也不能获取大小没有size属性。你只能对它们进行增、删、查操作。WeakMap 和 WeakSet 支持的 API操作WeakMapWeakSet添加set(key, value)add(value)删除delete(key)delete(value)查询get(key)/has(key)has(value)遍历❌ 不支持❌ 不支持获取大小❌ 无size❌ 无size3. 实际开发中的例子WeakSet 的实战记录已处理的对象假设你有一个业务需求页面上有很多商品卡片当用户鼠标悬停Hover时你要给卡片加一个高亮动画。但是同一个卡片只能触发一次高亮。如果用普通数组Array来记录随着页面操作数组会越来越长导致内存泄漏。这时候 WeakSet 就派上用场了// 创建一个 WeakSet 用来做标记consthighlightedCardsnewWeakSet();functionhandleHover(cardNode){// 如果已经高亮过直接返回if(highlightedCards.has(cardNode))return;// 执行高亮动画...doHighlightAnimation(cardNode);// 打上标记这个卡片已经处理过了highlightedCards.add(cardNode);}为什么这里用 WeakSet 最好如果这个商品卡片后来被下架了DOM 节点被移除垃圾回收器会自动清理掉这个节点。同时highlightedCards里的标记也会自动消失你完全不需要手动去数组里splice删除它。WeakMap 的实战缓存对象计算结果假设你有一个复杂的配置对象每次渲染前都需要对它进行哈希计算。为了避免重复计算我们用 WeakMap 做缓存constconfigCachenewWeakMap();functiongetComplexConfigHash(configObj){// 如果缓存里有直接返回if(configCache.has(configObj)){returnconfigCache.get(configObj);}// 进行耗时的哈希计算consthashperformHeavyHashCalculation(configObj);// 存入缓存configCache.set(configObj,hash);returnhash;}为什么这里用 WeakMap 最好当这个configObj在业务中被销毁时缓存也会随之自动清理绝不会造成内存堆积。补充说明WeakMap 在框架源码中也很常见比如 Vue 3 的响应式系统就大量使用 WeakMap 来存储对象与代理Proxy之间的映射关系确保源对象被销毁时对应代理也能被正确回收。
返回列表