如何使页面交互更流畅
流畅性
本篇是基于 FDCon2019 上《让你的网页更丝滑by刘博文》的复盘文。该课题也是博主感兴趣的领域, 后续会结合 React 的 Schedule 与该文进行进一步整合, 个人博客
- 被动交互: animation
- 主动交互: 鼠标、键盘
被动交互
当前市面上的设备频率在 60 HZ 以上。
主动交互
跑如下界面 https://code.h5jun.com/pojob
结合如下代码块, 可以看到 100ms 以下的点击是顺畅的, 而超过 100ms 的点击就会有卡顿现象。
var observer = new PerformanceObserver(function(list) { var perfEntries = list.getEntries() console.log(perfEntries) }); observer.observe({entryTypes: ["longtask"]});
让用户感觉到流畅
衡量一个网页/App 是否流畅有个比较好用的 Rail 模型, 它大概有以下几个评判标准值。
Response —— 100ms Animation —— 16.7ms Idle —— 50ms Load —— 1000ms
像素管道
像素管道一般由 5 个部分组成。JavaScript、样式、布局、绘制、合成。如下图所示:
保证主动交互让用户感觉流畅
function App() { useEffect(() => { setTimeout(_ => { const start = performance.now() while (performance.now() - start < 1000) { } console.log('done!') }, 5000) }) return ( <input type="text" /> ); }
一般超过 50 ms 认为是
long task(长任务),
long task会阻塞
main thread的运行, 如下是两种解决方案。
Web Worker
app.js代码如下:
import React, {useEffect} from 'react' import WorkerCode from './worker' function App() { useEffect(() => { const testWorker = new Worker(WorkerCode) setTimeout(() => { testWorker.postMessage({}) testWorker.onmessage = function(ev) { console.log(ev.data) } }, 5000) }) return ( <input type="text" /> ); }
worker.js代码如下:
const workerCode = () => { self.onmessage = function() { const start = performance.now() while (performance.now() - start < 1000) { } postMessage('done!') } }
此时在输入框输入时没有卡顿的感觉。
Time Slicing
下面是另外一种使页面流畅的方法 ——
Time Slicing(时间分片)。
观察 Chrome 的 Performance, 火焰图如下,
从火焰图可以看出主线程被拆分为了多个时间分片, 所以不会造成卡顿。时间分片的代码片段如下所示:
function timeSlicing(gen) { if (typeof gen === 'function') gen = gen() if (!gen || typeof gen.next !== 'function') return (function next() { const res = gen.next() // ① if (res.done) return // ⑤ setTimeout(next) // ③ })() } // 调用时间分片函数 timeSlicing(function* () { const start = performance.now() while (performance.now() - start < 1000) { console.log('执行逻辑') yield // ② } console.log('done') // ④ })
该函数虽然代码量不长, 但却不易理解。前置知识 Generator
下面对该函数进行分析:
- 往时间分片函数
timeSlicing
中传入generator
函数; - 函数的执行顺序 —— ①、②、③、① (此时有个竞赛的关系, 如果
performance.now() - start < 1000
则继续 ②、③, 如果performance.now() - start >= 1000
则跳出循环执行 ④、⑤);
conclusion
针对
long task会阻塞
main thread的运行的情形, 给出两种解决方案:
Web Worker
: 使用Web Worker
提供的多线程环境来处理long task
;Time Slicing
: 将主线程上的long task
进行时间分片;
保证被动交互让用户感觉流畅
保证
16.7ms有新的一帧传输到界面上。除去用户的逻辑代码, 一帧内留给浏览器整合的时间大概只有
6ms左右, 回到像素管道上来, 我们可以从这几方面进行优化:
避免 CSS 选择器嵌套过深
Style 这部分的优化在 css 样式选择器的使用, css 选择器使用的层级越多, 耗费的时间越多。以下是测试 css 选择器不同层级筛选相同元素的一次测试结果。
div.box:not(:empty):last-of-type span 2.25ms index.html:85 .box--last span 0.28ms index.html:85 .box:nth-last-child(-n+1) span 2.51ms
避免布局重排
// 先修改值 el.style.witdh = '100px' // 后取值 const width = el.offsetWidth
这段代码有什么问题呢?
可以看到它会造成布局重排。
应对的策略是调整它们的执行顺序,
// 先取值 const width = el.offsetWidth // 后修改值 el.style.witdh = '100px'
可以看到经过调换顺序后, 后执行的 el.style.width 会新开一个像素管道, 而不会在原先的像素管道进行重排。
此外不要在循环中执行如下的操作,
for (var i = 0; i < 1000; i++) { const newWidth = container.offsetWidth; // ① boxes[i].style.width = newWidth + 'px'; // ② }
可以在火焰图中看到它发生了重绘的警告,
执行顺序是 ①②①②①②①..., 假若我们在第一个 ① 后面插入一条竖线后 ①|②①②①②①, 其就变成先修改值后取值的情景, 所以也就发生了重绘!
正确的使用姿势应该如下:
const newWidth = container.offsetWidth; for (var i = 0; i < 1000; i++) { boxes[i].style.width = newWidth + 'px'; }
避免重绘
创建 Layers(图层) 可以避免重绘,
{ transform: translateZ(0); }
- 如何实现WebView和js页面的交互
- 两种交互布局如何在同一页面显示?
- struts中如何用gson和AJAX与jsp页面交互
- 如何让语音交互更流畅?
- 如何实现iframe页面与父级页面js交互
- 如何实现WebView和js页面的交互
- 微信小程序如何与java后台交互,获取json数据显示到页面,求大神指点
- 如何给webApp应用添加动态水印?并且不影响页面的交互事件
- android中的代码如何与html页面交互
- 如何使用php实现首页和子页面之间的交互
- 新手如何正确开启页面与后台数据的交互之路
- springMvc如何和前端页面交互,传输json数据
- 新手如何正确开启页面与后台数据的交互之路
- SkylineGlobe的PopupMessage里面嵌入的网页如何与主页面交互通讯
- Skyline软件二次开发初级——9如何在WEB页面中的三维地图上进行交互
- web开发中前端页面是如何跟后端服务器数据交互的
- 如何在struts2中登录后跳转到前置页面
- 如何禁止页面缓存
- iOS如何用代码控制以不同屏幕方向打开新页面?
- jsp页面如何接受post和get的数据