- 前端
- 移动开发
【免费下载链接】Mars
腾讯移动 Web 前端知识库
导读:本文以腾讯移动 Web 前端知识库 Mars 仓库的 performance 性能专题 为主体,系统讲解移动端高性能 Web 开发的三条主线——动画技术选型、GPU 加速与动画防闪、DOM layout 读写批处理与 CSS 动画属性性能,并结合仓库内的源码级注释、问题清单与触摸事件 Demo 做交叉印证。读完本文,你将掌握从「CSS3 动画为什么更快」到「哪些操作会触发 layout」的完整性能优化链路,并能在自己的移动端项目里直接落地translate3d加速、backface-visibility防闪、layout-queue 批量读写等实战方案。
一、性能专题的定位:从 PC 到 Mobile 的三维考量
Mars 的 性能目录 将「高性能 Mobile Web 开发」组织为四大方向,构成了移动端性能优化的完整知识地图:
- 高性能 CSS3 动画(对应 high-performance-css3-animation.md);
- CSS 动画属性性能(对应 css-property-animation-performance.md);
- 高性能滚动:核心思路是通过 passive event listener 降低滚动事件对渲染主线程的阻塞,让滚动过程中的事件监听默认不再调用
preventDefault(),从而避免浏览器为等待可能的拦截而被迫同步执行,减少滚动卡顿; - 离线 Web App 以及消息同步与推送:基于 Service Worker 实现页面资源离线缓存、后台消息同步与推送能力,这是移动网络环境下降低首屏等待与流量损耗的关键基建。
与 PC 时代「只关注流畅度」不同,移动端需要额外关注两个维度:
- 流量:用户基于运营商基站网络访问,页面体积、资源加载策略直接关系到用户的流量账单;
- 功耗:设备电池有限,动画渲染策略不当会导致耗电激增,例如无节制的 3D 变形会持续占用 GPU 资源;
- 流畅度:这是 PC 与移动端共同的核心指标,主要体现在前端动画中。
二、动画技术选型:为什么移动端优先 CSS3 动画
在现有前端动画体系中,通常有两种模式:
- JS 动画:通过 JS 动态改写样式实现动画。在 PC 端兼容低版本浏览器时是一种推荐方案,但每一帧的样式改写都要经过 JS 引擎 → 样式计算 → 布局 → 绘制的完整链路,性能受制于脚本执行效率;
- CSS3 动画:由浏览器原生实现,渲染管线完全在浏览器内部调度,无需 JS 逐帧介入。
移动端终端性能与 PC 存在显著差距,因此 Mars 给出的结论是:在移动端选择性能更优的浏览器原生实现方案——CSS3 动画。不过 CSS3 动画在移动多终端场景下依然会面对比 PC 更多的性能问题,主要体现在动画的卡顿与闪烁,需要在下面几个层面逐一治理。
三、充分利用硬件能力:用 3D 变形开启 GPU 加速
开启 GPU 加速的最常用手段,是给动画元素施加一个恒定的 3D 变形。Mars 文档给出的标准写法(见 high-performance-css3-animation.md):
-webkit-transform: translate3d(0, 0, 0); -moz-transform: translate3d(0, 0, 0); -ms-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);translate3d(0, 0, 0)本身没有位移效果,但其作用在于将元素提升到独立的合成层(composite layer),让后续的transform动画由 GPU 合成而非 CPU 逐帧绘制,动画流畅度显著提升。
权衡提示(文档原意):3D 变形会消耗更多的内存与功耗。Mars 明确提醒「应确实有性能问题时才去使用它,兼在权衡」——不要为了炫技而对所有元素无差别施加 3D 变形,否则会在多终端上造成内存占用与电量损耗的副作用。
四、消除动画闪烁:backface-visibility 与 perspective 的 Hack
动画过程中(尤其是动画开始瞬间)常出现闪白/闪烁,Mars 给出了如下 Hack 组合(见 high-performance-css3-animation.md):
-webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000; -moz-perspective: 1000; -ms-perspective: 1000; perspective: 1000;原理上,backface-visibility: hidden隐藏元素的背面,perspective建立透视投影,二者组合能够稳定元素的 3D 渲染上下文,减少合成层在动画首帧被重新创建时产生的闪烁。
这条 Hack 在仓库的 问题清单 issues/README.md 中也有完全一致的印证——「消除 transition 闪屏」条目给出了另一种等价组合:
-webkit-transform-style: preserve-3d; /* 设置内嵌的元素在 3D 空间如何呈现:保留 3D */ -webkit-backface-visibility: hidden; /* 设置进行转换的元素的背面在面对用户时是否可见:隐藏 */并且明确指出「动画过程中的动画闪白可以通过 backface-visibility 隐藏」。可见这一组属性是 Mars 团队在真实移动端项目中反复验证过的「防闪标配」。
五、实战对比:translate3d 为什么比 left 更流畅
Mars 用一个双 ball 的对比示例直观说明差异(见 high-performance-css3-animation.md):
/* 方案一:使用 transform 完成右移 500px */ #ball-1 { transition: -webkit-transform .5s ease; -webkit-transform: translate3d(0, 0, 0); } #ball-1.slidein { -webkit-transform: translate3d(500px, 0, 0); } /* 方案二:使用 left 完成右移 500px */ #ball-2 { transition: left .5s ease; left: 0; } #ball-2.slidein { left: 500px; }两者视觉结果相同,但渲染开销天差地别:
- 修改
left会改变元素的几何位置,浏览器必须重新计算布局(layout/reflow),再重绘(repaint),最后合成(composite)——链路长、开销大; - 修改
transform不改变文档流中的几何信息,元素已在合成层中,浏览器只需做一次合成(composite),GPU 直接完成位移动画。
这一结论在仓库 issues/README.md 中同样被列为移动端问题清单的一项:「动画效果中,使用 translate 比使用定位性能高」。可以说这是整个性能专题反复强调的第一铁律。
六、规避渲染代价高的属性:box-shadow、gradients 与文档流
6.1 远离 box-shadow 与 gradients
box-shadow与gradients往往都是页面的性能杀手,尤其是在一个元素同时使用两者时,阴影与渐变的绘制成本会叠加,直接影响首屏与动画帧率。Mars 的应对策略非常干脆:「拥抱扁平化设计吧」(见 high-performance-css3-animation.md)——减少高成本绘制属性,是移动端视觉设计的隐性能约束。
6.2 让动画元素脱离文档流,减少重排
动画元素若处于文档流中,其位置变化会连锁引发周围元素的重排。让动画元素脱离文档流可显著缩小 layout 的影响范围(见 high-performance-css3-animation.md):
position: fixed; position: absolute;七、DOM layout 性能优化:layout-queue 与读写批处理
这是 Mars 性能文档中篇幅最长、也最具实战价值的一节。先看两段能力上完全等价、执行顺序不同的代码(见 high-performance-css3-animation.md):
// 触发两次 layout var newWidth = aDiv.offsetWidth + 10; // Read aDiv.style.width = newWidth + 'px'; // Write var newHeight = aDiv.offsetHeight + 10; // Read aDiv.style.height = newHeight + 'px'; // Write // 只触发一次 layout var newWidth = aDiv.offsetWidth + 10; // Read var newHeight = aDiv.offsetHeight + 10; // Read aDiv.style.width = newWidth + 'px'; // Write aDiv.style.height = newHeight + 'px'; // Write规律总结:把连续的「读取」聚合在一起,再把连续的「写入」聚合在一起,相比「读写交替」可少触发一次 layout。
其底层原因是浏览器的优化策略:所有可触发 layout 的操作都会被暂时放入layout-queue(布局队列)中,等到「必须更新」的时机,浏览器一次性计算整个队列中所有操作影响的结果,从而只进行一次 layout。如果读写交替,每次读操作都会因为「必须读取最新值」而强制 flush 队列,layout 次数随之增加。
这条规律直接对应到移动端动画场景:在 touchmove 或 rAF 回调中操作 DOM 几何属性时,务必「先集中读取、后集中写入」,否则每帧都会付出多次 layout 的代价。
八、源码级追问:哪些操作会触发 layout
「等到必须更新的时候」——这个必要条件是什么?Mars 从开源浏览器内核实现入手给出了答案。以开源 Webkit/Blink 为例,layout 的更新主要通过Document::updateLayout与Document::updateLayoutIgnorePendingStylesheets两个方法完成,源码如下(见 high-performance-css3-animation.md):
void Document::updateLayout() { ASSERT(isMainThread()); FrameView* frameView = view(); if (frameView && frameView->isInLayout()) { ASSERT_NOT_REACHED(); return; } if (Element* oe = ownerElement()) oe->document()->updateLayout(); updateStyleIfNeeded(); StackStats::LayoutCheckPoint layoutCheckPoint; if (frameView && renderer() && (frameView->layoutPending() || renderer()->needsLayout())) frameView->layout(); if (m_focusedNode && !m_didPostCheckFocusedNodeTask) { postTask(CheckFocusedNodeTask::create()); m_didPostCheckFocusedNodeTask = true; } } void Document::updateLayoutIgnorePendingStylesheets() { bool oldIgnore = m_ignorePendingStylesheets; if (!haveStylesheetsLoaded()) { m_ignorePendingStylesheets = true; HTMLElement* bodyElement = body(); if (bodyElement && !bodyElement->renderer() && m_pendingSheetLayout == NoLayoutWithPendingSheets) { m_pendingSheetLayout = DidLayoutWithPendingSheets; styleResolverChanged(RecalcStyleImmediately); } else if (m_hasNodesWithPlaceholderStyle) recalcStyle(Force); } updateLayout(); m_ignorePendingStylesheets = oldIgnore; }从实现可知,updateLayoutIgnorePendingStylesheets是对updateLayout的扩展,且在现有 layout 更新模式中,大部分场景都是调用前者。基于内核源码,Mars 归纳出了以下会触发 layout 的操作清单(即被强制 flush layout-queue 的读取点):
Element:clientHeight、clientLeft、clientTop、clientWidth、focus()、getBoundingClientRect()、getClientRects()、innerText、offsetHeight、offsetLeft、offsetParent、offsetTop、offsetWidth、outerText、scrollByLines()、scrollByPages()、scrollHeight、scrollIntoView()、scrollIntoViewIfNeeded()、scrollLeft、scrollTop、scrollWidth;Frame, HTMLImageElement:height、width;Range:getBoundingClientRect()、getClientRects();SVGLocatable:computeCTM()、getBBox();SVGTextContent:getCharNumAtPosition()、getComputedTextLength()、getEndPositionOfChar()、getExtentOfChar()、getNumberOfChars()、getRotationOfChar()、getStartPositionOfChar()、getSubStringLength()、selectSubString();SVGUse:instanceRoot;window:getComputedStyle()、scrollBy()、scrollTo()、scrollX、scrollY、webkitConvertPointFromNodeToPage()、webkitConvertPointFromPageToNode()。
实战含义非常直接:在「写入样式」与「读取这些几何/计算属性」之间,尽量保持「先读后写」的批处理顺序;同时动画循环内避免调用getComputedStyle()、offsetWidth等会强制同步 layout 的接口。
九、CSS 动画属性性能:relayout / repaint / recomposite 三阶段
如果说上一节回答的是「什么会触发 layout」,那么 CSS动画属性性能 回答的则是「动画属性本身走哪条渲染管线」。核心结论有三条:
- CSS 动画属性会触发整个页面的重排(relayout)、重绘(repaint)、重组(recomposite);
- Paint(绘制)通常是其中最花费性能的环节,应尽可能避免使用触发 paint 的 CSS 动画属性;
- 这正是推荐用
-webkit-transform: translateX(3em)代替left: 3em的原因:left会额外触发 layout 与 paint,而transform只触发整个页面的 composite。
文档给出了一个完整可运行的对比实验(见 css-property-animation-performance.md)。基础样式如下:
div { -webkit-animation-duration: 5s; -webkit-animation-name: move; -webkit-animation-iteration-count: infinite; -webkit-animation-direction: alternate; width: 200px; height: 200px; margin: 100px; background-color: #808080; position: absolute; }用left驱动的 keyframes(原文档配图显示页面被持续触发重绘,以红色边框标记):
@-webkit-keyframes move{ from { left: 100px; } to { left: 200px; } }用-webkit-transform驱动的 keyframes(原文档配图显示页面只发生重组,以橙色边框标记):
@-webkit-keyframes move{ from { -webkit-transform: translateX(100px); } to { -webkit-transform: translateX(200px); } }两种写法在 5 秒往返无限循环的动画中表现截然不同,前者每秒多出大量 layout + paint 工作,后者仅需合成层位移。这一实验结论可以进一步推广为一份决策表:动画涉及width/height/left/top/margin等布局属性时走最重的管线;涉及transform/opacity时走最轻的管线。原文档中附有「CSS 属性在 CSS 动画中行为表」,即在动画中逐属性标注其触发的渲染阶段,选属性前先查表是移动端动画设计的基本功。
十、仓库内的实践印证:问题清单与触摸事件 Demo
性能专题的结论并非孤例,Mars 仓库的其它目录提供了大量一线印证。
10.1 问题清单中的性能条目
在 issues/README.md(iOS 与 Android 平台问题列表)中,与性能直接相关的条目包括:
- translate 优于定位(issues/README.md):「动画效果中,使用 translate 比使用定位性能高」,与专题铁律完全一致;
- 滚动事件的高频回调处理(issues/README.md):绑定
touchmove时若回调中处理内容较多,FPS 会下降;把代码包进setTimeout(..., 0)延迟到下一轮事件循环执行,程序反而会变快:
// 处理量大的写法:FPS 易下降 $('div').on('touchmove', function(){ //.….code }); // 推荐写法:将重活推迟到 setTimeout 中执行 $('div').on('touchmove', function(){ setTimeout(function(){ //.….code },0); });- 滚动位置读取(issues/README.md):
window.scrollY/window.scrollX属于前文清单中的 layout 触发点,读取时机应与写入批处理; - tap 事件的构成(issues/README.md):移动端 click 存在普遍约 300ms 延迟,开发者大多使用由
touchstart+touchmove判断 +touchend封装而成的 tap 事件替代——这与高性能滚动专题中「减少事件处理对渲染的阻塞」是同一主题的两面。
10.2 触摸事件 Demo:验证合成层加速的载体
仓库 demos/touchevents.html 提供了一个可直接在移动浏览器打开的触摸事件实验页,其中#touch元素在样式中就应用了-webkit-transform: translate3d(0,0,0)来开启合成层,同时通过ontouchstart、ontouchmove、ontouchend、ontouchcancel、onclick记录各事件的时间戳并输出 touchstart 到 click 的差值。这个 Demo 一方面验证了触摸事件序列,另一方面也演示了「动画/交互元素常驻合成层」的实际写法,可与本专题的 GPU 加速方案配合使用。
十一、专题知识地图的另外两块拼图
回到 performance/README.md 的知识地图,除了两篇深度文档,还有两个方向值得纳入移动端性能优化全景:
- 高性能滚动:核心思路是采用 passive event listener——让滚动/触摸事件监听默认不调用
preventDefault(),避免浏览器为了等待拦截而必须同步执行监听器,从而减少滚动过程中对渲染主线程的阻塞;结合上文touchmove的setTimeout技巧,可显著改善长列表滚动帧率; - 离线 Web App 与消息同步、推送:基于 Service Worker 实现资源离线缓存、后台消息同步与推送,把移动网络下的首屏加载与流量损耗问题前置解决。这一方向与前文「流量、功耗、流畅度」的三维考量直接呼应。
十二、落地检查清单
综合全文,给出可直接照做的移动端高性能开发清单:
- 动画优先使用
transform(含translate3d开启 GPU 加速),避免用left/top/width/height/margin驱动动画; - 动画元素统一
position: absolute/fixed脱离文档流,缩小重排影响面; - 动画开始闪白时,叠加
backface-visibility: hidden、perspective(或transform-style: preserve-3d); - 慎用
box-shadow与gradients,避免同元素叠加使用,倾向扁平化设计; - JS 操作 DOM 几何时「先集中读、后集中写」,利用 layout-queue 把每帧 layout 次数压到最低;
- 动画循环中避免读取
offsetWidth/offsetHeight/getComputedStyle/scrollTop等强制同步 layout 的属性; touchmove/scroll高频回调中,把重活推迟到setTimeout(..., 0);- 3D 变形有内存与功耗成本,仅在确有性能问题时使用,权衡取舍。
相关阅读(仓库内)
- 性能专题目录
- 高性能 CSS3 动画:GPU 加速、防闪与 layout 优化
- CSS 动画属性性能:relayout/repaint/recomposite 决策
- iOS 与 Android 平台问题列表::active、transition 闪屏、300ms 延迟等
- 触摸事件 Demo:合成层加速的交互实验页
- Mars 项目主 README:知识库全景入口
- 前端
- 移动开发
【免费下载链接】Mars
腾讯移动 Web 前端知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考