zoukankan      html  css  js  c++  java
  • 如何使页面交互更流畅

    流畅性

    本篇是基于 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

    下面对该函数进行分析:

    1. 往时间分片函数 timeSlicing 中传入 generator 函数;
    2. 函数的执行顺序 —— ①、②、③、① (此时有个竞赛的关系, 如果 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);
    }
    
  • 相关阅读:
    选择主要的构建实践方法(转) Tech
    201671010109 201620172《java程序设计》第一周感想
    201671010109 201720162第二周学习感想
    2016710101090 20162017《java程序设计》第三周感想
    sort k 详解
    java.util.NoSuchElementException: None.get的解决方法
    Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace 解决方法
    Java学习随笔之1: Java 语言概述和开发环境
    java 学习随笔之2:理解面向对象
    Selenium Basic Knowledge
  • 原文地址:https://www.cnblogs.com/MuYunyun/p/10924924.html
Copyright © 2011-2022 走看看