7.3 性能优化与常见错误 性能问题很少以"整页卡死"的姿态出现,它们更像慢性渗血:选择器写得太宽、循环里反复查询、动画动错了属性——单次都无感,叠加起来页面就沉了。本节收拢前六章散落的性能线索,给出四个止血点与一份常见错误清单;每个点都能直接在标本页上量出差别。 本节摘要:四个止血点——选择器缓存与收窄、批量操作离线做、事件委托替代逐个绑定、布局读写在时间上分离;常见错误清单覆盖重复绑定、隐式全局变量、选择器拼写静默失败、滥用 重建等高频劣习。 止血点一:选择器缓存与收窄 同一个选择器查两遍,就是付两遍钱。把重复使用的查询存进变量,并把查询范围收窄到最近的容器: 第二行改写还揭示了更深的规则:jQuery 方法天然作用于整个集合,很多"循环加方法"的写法可以整体塌缩成一行集合调用。
性能问题很少以"整页卡死"的姿态出现,它们更像慢性渗血:选择器写得太宽、循环里反复查询、动画动错了属性——单次都无感,叠加起来页面就沉了。本节收拢前六章散落的性能线索,给出四个止血点与一份常见错误清单;每个点都能直接在标本页上量出差别。
本节摘要:四个止血点——选择器缓存与收窄、批量操作离线做、事件委托替代逐个绑定、布局读写在时间上分离;常见错误清单覆盖重复绑定、隐式全局变量、选择器拼写静默失败、滥用
html()重建等高频劣习。
同一个选择器查两遍,就是付两遍钱。把重复使用的查询存进变量,并把查询范围收窄到最近的容器:
// 反面:循环里每次都全文档查 for (var i = 0; i < 100; i++) { $('#queue .item').eq(i).addClass('seen'); } // 正面:查一次、存变量、循环内复用 var $items = $('#queue').find('.item'); $items.addClass('seen'); // 更好:集合方法天生批量,循环都省了
第二行改写还揭示了更深的规则:jQuery 方法天然作用于整个集合,很多"循环加方法"的写法可以整体塌缩成一行集合调用。选择器本身的宽窄也计入成本——$('.item') 全文档海选与 $('#queue').find('.item') 容器内查找,前者多付整棵树的遍历费;ID 打头的组合选择器最快,因为第一段就用 getElementById 把范围钉死了(呼应 2.1 的刀法代价表)。
往文档里插一百个条目,逐条插入就是一百次布局计算;先把零件在"体外"组装成型,最后一口气入体,布局只算一次:
// 反面:逐条 append,每次都可能触发重排 list.forEach(function (p) { $('#queue').append('<li class="item">' + p.name + '</li>'); }); // 正面:离线拼装,一次入体 var html = list.map(function (p) { return '<li class="item">' + p.name + '</li>'; }).join(''); $('#queue').html(html);
规模小的时候两条路都无感,量到几百上千条时差距立现。更讲究的变体是先用 detach 把容器摘下、在体外改完再接回——适合"改的是现有节点而不是新建"的场景。反面是这一招的搭档条款:能用 text 或模板复用就别高频 html(),字符串解析加节点重建在循环里是双重开销(6.3 节选型推演里已有伏笔)。
内存与登记成本随监听器数量线性上涨,4.4 节的委托术就是为此而生:千条列表一份监听。补一条性能视角的新账——委托选择器的筛选发生在每次事件上,选择器写得太深会把省下的绑定钱又赔进筛选里,粒度选最近的公共容器(4.4 的结论在这里二次生效)。
浏览器对布局的读写是"一改全算":读过一次几何值(offset、width、:visible 判定),紧接着又改样式,下一次读就强制重新排版。把"读"集中在前、"写"集中在后,能把十次重排压成一次:
// 反面:读写交替,每次读都被迫重排 $rows.each(function () { var h = $(this).height(); // 读 $(this).height(h + 20); // 写 }); // 正面:先全读,再全写 var hs = $rows.map(function () { return $(this).height(); }).get(); $rows.each(function (i) { $(this).height(hs[i] + 20); });
动画场景同理:5 章说过的"动画别动布局属性"(宽度、高度、位置)优先改 transform 与透明度,原因正是布局属性每帧重排,合成属性每帧零排版——现代 CSS 动画流畅的底层原因,与这里是同一条物理规律。

性能之外的劣习按"症状—病因—处方"收成一张清单,多数在本册各章的坑位里出现过:
css 内联层被样式表 important 压制。处方:先分清手里是衣还是人。attr('value') 读初始快照(3.2)。处方:一律 val()。console.log(length)。var 的隐式全局变量。处方:严格模式加代码检查工具兜底。四个止血点都是经验证的套路,但套用之前先确认"血到底从哪渗"——优化的第一课是测量。浏览器自带秒表,两行代码就能把嫌疑段落圈出来:
var t0 = performance.now(); renderHugeList(data); // 嫌疑函数 console.log('渲染耗时', (performance.now() - t0).toFixed(1), '毫秒');
把秒表分别套在"查询、拼装、插入、绑定"四段上,再去性能面板对照火焰图里的长条,渗血点往往与直觉不符:自以为的"选择器慢"测出来是零点几毫秒,真凶是循环里的逐条插入。原则一句话:先测量、再动手、改完复测——没有复测的优化等于没做,因为你无法证明它生效了。
背景:标本页接入真实数据后,刷新要卡上一阵才见内容,团队口头诊断分成"选择器太宽"与"数据量太大"两派。
操作:第一段测查询与拼装,两端各挂秒表——查询端零点几毫秒出局,拼装端耗时也有限;第二段打开性能面板重录一次刷新,火焰图里最长的一条标着布局计算,触发点密集出现在插入循环里;第三段按止血点二把逐条插入改成离线拼装一次入体,复测刷新耗时直接掉到原先的零头。
结果与解读:两派口头诊断都不对,真凶是重排。这个病例的通用教训是:性能问题的因果链埋在浏览器内部,肉眼与直觉都不可靠,秒表加火焰图才是证据链;四个止血点各自对应一类证据特征——长条在布局段查插入方式、脚本段查查询与循环、事件段查监听数量——按证据选止血点,不按流行度。
流血点止住了。下一节把代码从"能跑"组织成"可交接":模块化的码法与一查到底的调试流程。