性能优化(二) 渲染阶段性能优化

1,750 阅读6分钟

背景

  • 本文主要是针对渲染环节进行优化
  • 本文给出的优化方向是经过筛选的,一些过老的点就不重复提了
  • 本文给出的案例是结合开发中的实际经验,比较基础,如果需要学习webworkerrequestAnimationFrame这类实际项目里使用场景比较少的方法,需要自行去参考文献查看
  • 选取的参考文件是比较有代表性和实效性的,如果有更好的资料,欢迎交流

系列文章

性能优化(一) 页面性能参数解析及采集

关键渲染路径(CRP)

image.png

CRP各阶段性能瓶颈及解决方案

1. 根据 HTML 资源构建 DOM 树、根据 CSS 资源构建 CSSOM 树,二者并行

在这个阶段,会同时进行 DOM 树、CSSOM 树的构建,优化方向主要是提高 HTMLCSS 解析效率以及减少因为加载 JSCSS 资源造成的阻塞。

HTML

  1. 避免过度使用注释

构建时统一清除

  1. 组织好 DOM,尽量只创建绝对必要的元素

  2. 关闭自我封闭的标签

//不推荐
<br/>

//推荐
<br></br>

CSS

  1. 尽可能使用类选择器来代替标签选择器

CSS 查找样式表时是从右往左查询的,当遇到一个标签选择器如 p 时,会先遍历页面里所有的 p 标签,然后再从这些 p 标签里找父元素包含类名 item 的目标,这个过程遍历了很多不会用到的标签,所以尽可能用类选择器来提高查询效率

//不推荐
.item p{
    color:red;
}

//推荐
.item_p{
    color:red;
}
  1. 尽可能减少嵌套,并且用 BEM 规范来命名选择器

多层嵌套的 CSS 在解析过程中很耗费资源,尽可能保证不超过三层,另外可以用 BEM 规范来命名选择器

BEM规范 BEM是一种CSS的书写规范,它的名称是由三个单词的首字母组成的,分别是块(Block)、元素(Element)和修饰符(Modifier)。理论上它希望每行CSS代码只有一个选择器,这就是为了降低选择器的复杂性,对选择器的命名要求通过以下三个符号的组合来实现。

  • 中画线(-):仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。
  • 单下画线(--):作为描述一个块或其子元素的一种状态。
  • 双下画线(__):作为连接块与块的子元素。
//不推荐
.list {
  .item {
    p {
      .normal {
        color: red;
      }
    }
  }
}

//推荐
.list__item__p--normal {
  color: red;
}
  1. 减少 CSS 冗余代码

减少 CSS 资源体积,可以提交加载和解析速度。可以使用 analyze-css 来分析和更正CSS代码,可优化地方都会在 offenders 数组中标注

//安装依赖
npm install --global analyze-css


//根据文件路径分析css文件
analyze-css --file src/test/style.scss



//结果会打印在控制台,metrics是关键参数,offenders是具体可优化的地方
{
  "generator": "analyze-css v0.10.2",
  "metrics": {
    "base64Length": 11332,
    "redundantBodySelectors": 0,
    "redundantChildNodesSelectors": 1,
    "colors": 106,
    "comments": 1,
    "commentsLength": 68,
    "complexSelectors": 37,
    "duplicatedSelectors": 7,
    "duplicatedProperties": 24,
    "emptyRules": 0,
    "expressions": 0,
    "oldIEFixes": 51,
    "imports": 0,
    "importants": 3,
    "mediaQueries": 0,
    "notMinified": 0,
    "multiClassesSelectors": 74,
    "parsingErrors": 0,
    "oldPropertyPrefixes": 79,
    "propertyResets": 0,
    "qualifiedSelectors": 28,
    "specificityIdAvg": 0.04,
    "specificityIdTotal": 25,
    "specificityClassAvg": 1.27,
    "specificityClassTotal": 904,
    "specificityTagAvg": 0.79,
    "specificityTagTotal": 562,
    "selectors": 712,
    "selectorLengthAvg": 1.5722460658082975,
    "selectorsByAttribute": 92,
    "selectorsByClass": 600,
    "selectorsById": 25,
    "selectorsByPseudo": 167,
    "selectorsByTag": 533,
    "length": 55173,
    "rules": 433,
    "declarations": 1288
  },
  
  "offenders": {
    "multiClassesSelectors": [
      ".test-card.test-report-title @ 3:1",
      ".test-report-page.test-report-table @ 32:1",
      ".test-report-page.test-report-table @ 38:1",
      ...
    ],

    "complexSelectors": [
      ".test-report-page .test-report-table td @ 38:1",
      ".test-report-page .test-report-table th @ 38:1",
      ...
    ],
    "qualifiedSelectors": [
      ".test-report-page .test-report-table td @ 38:1",
      ".test-report-page .test-report-table th @ 38:1",
       ...
    ],
    "redundantChildNodesSelectors": [
      ".test-report-page .test-report-table thead tr th @ 58:1",
      ".test-report-page .test-report-table thead tr th:first-child @ 63:1",
      ".test-report-page .test-report-table thead tr th:last-child @ 67:1"
       ...
    ]
  }
}
  1. 加载远程字体时使用font-display可以避免出现文本空白的情况,需要搭配 @font-face使用

    font-display - CSS(层叠样式表) | MDN

    页面字体闪一下?这两个标准能帮到你 - 掘金

/* 关键字值 */
font-display: auto; //字体显示策略由用浏览器定义。

font-display: block; //浏览器首先使用隐形文字替代页面上的文字,并等待字体加载完成再显示;

font-display: swap; //如果设定的字体还未可用,浏览器将首先使用备用字体显示,
当设定的字体加载完成后替换备用字体;

font-display: fallback; //与 swap 属性值行为上大致相同,
但浏览器会给设定的字体设定加载的时间限制,一旦加载所需的时长大于这个限制,
设定的字体将不会替换备用字体进行显示。 Webkit 和 Firefox 中设定此时间为 3sfont-display: optional; //使用此属性值时,如果设定的字体没有在限制时间内加载完成,
当前页面将会一直使用备用字体,并且设定字体继续在后台进行加载,以便下一次浏览时可以直接使用设定的字体。


//示例
@ font-face {
  font-family: ExampleFont;
  src: url(/path/to/fonts/examplefont.woff)format('woff'),
       url(/path/to/fonts/examplefont.eot)format('eot');
  font-weight: 400;
  font-style: normal;
  font-display: fallback;
}
  1. 媒体查询

涉及尺寸变化、打印或朝向变动等情况,优先加载关键资源

媒体查询入门指南 - 学习 Web 开发 | MDN

  1. 避免使用 @import 引入CSS

@import 引入的 CSS 需要依赖包含的 CSS 被下载与解析完毕才能被发现,增加了关键路径中的往返次数,意味着浏览器引擎的计算负载加重。

JS

  1. 对于非关键资源,可以延迟加载(async、defer)
  • 用到时再加载

可以借助 Webpack Bundle Analyzer 包分析器来查看哪些资源是不关键的# Webpack Bundle Analyzer包分析器

可以借助 Suspence + React.lazy() + @babel/plugin-syntax-dynamic-import 实现借助 React.lazy() 和 import() 实现代码分割

  • 场景一:路由变化时的加载
<Route
  path={`${match.url}/test`}
  component={lazy(() => import('./test'))}
/>
  • 场景二:页面内加载

例如列表页的卡片、模态框组件都可以等待请求完成后再加载

const CardItem = React.lazy(() => import('../component/CardItem'))

cardData.length > 0 && <CardItem />
  • 资源空闲时再加载

可以借助 React18useDeferredValue,可以确保页面中最关键的部分先更新,其他组件延迟更新

  1. 利用 webworker 分担 JS 内核的压力

2. Layout和Paint

这个阶段也称回流和重绘,这个阶段的优化就是尽可能的少触发元素的几何信息及样式变化。

  1. 将多次样式更改、位置信息变更、元素尺寸信息变更存储起来,然后统一修改
  • 用变量缓存更改结果
//不推荐
<div id="muxin">木鑫</div>
<script>
  const muxin = document.getElementById('muxin')
  for (let i = 0; i < 10; i++) {
    muxin.style.color = randomColor()
    muxin.style.top = `${muxin.offsetTop + 16}px`
  }
</script>


//推荐
<div id="muxin">木鑫</div>
<script>
  const muxin = document.getElementById('muxin')
  for (let i = 0; i < 10; i++) {
    finalColor = randomColor()
    finalTop = `${muxin.offsetTop + 16}px`
  }
  muxin.style.color = finalColor
  muxin.style.top = finalTop
</script>
  • 频繁改动一个元素时,可以通过 display 先拿掉,等到最终样式计算出来再展示,这样只会触发一次回流和重绘
<div id="muxin">木鑫</div>
<script>
  const muxin = document.getElementByid('muxin')
  muxin.style.display = 'none'
  muxin.style.color = 'red'
  muxin.style.height = '32px'
  muxin.style.fontSize = '16px'
  muxin.style.padding = '4px'
  muxin.style.display = 'visible'
</script>
  • 以上两点都是尽可能将短时间内多次会触发回流和重绘的变动存储起来,到合适的时间一起更改。其实浏览器本身已经实现了该优化,就是 Flush 队列,但是如果计算过程中涉及offsetwidth、offsetheight、width、height等属性时,还是会触发回流,因为这些属性具有实时性,必须通过不断的回流才能得到最新状态
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <div id="muxin">muxin 性能优化</div>
  </head>



  <body>
    <script>
      const muxin = document.getElementByid('muxin')
      muxin.style.color = 'red'
      muxin.style.height = '32px'
      muxin.style.fontSize = '16px'
      muxin.style.padding = '4px'
    </script>
  </body>
</html>

效果图如下:

  1. 动画的优化 在动画实现方面,CSStransitionsanimations 或者 JSrequestAnimationFrame 都是比较好的选择。其中 JSsetInterval 实现动画是反面教材,因为他是一个宏任务,执行起来很可能有延迟,并且执行频率跟屏幕的刷新频率不一致,会造成视觉上的卡顿,requestAnimationFrame 的优势在于自动以系统刷新频率来选择调用时机,不会延迟执行、也不会有页面卡顿的现象,而且只有在页面激活时才生效,不需要额外卸载定时器。 CSS and JavaScript animation performance - Web 性能 | MDN

  2. 拆分不同图层,将不变化的图层放到一个新图层里,例如将动画提到 GPU 运行,这是一种用高耗电、高内存换渲染效率的一种方法 CSS performance optimization - 学习 Web 开发 | MDN

CSS最佳实践

Airbnb CSS-in-JavaScript编码规范-中文

airbnb-javascript-style-guide-cn/css-in-javascript at master · libertyAlone/airbnb-javascript-style-

Airbnb CSS / Sass 编码规范-中文

github.com/Zhangjd/css…

其中例如格式化工作由 lint 自动规范、ID选择器默认不使用已成为规约,不需要额外摘出来,所以下面只介绍一些我认为需要额外注意的的地方:

  1. 类选择器命名

类名建议使用破折号代替驼峰法。如果你使用 BEM(见上文),也可以使用下划线。

  1. 注释
  • 建议使用行注释 (在 Sass 中是 //) 代替块注释。
  • 建议注释独占一行。避免行末注释。
  • 给没有自注释的代码写上详细说明,比如:

    • 为什么用到了 z-index
  • 兼容性处理或者针对特定浏览器的 hack
  1. 边框
  • 在定义无边框样式时,使用 0 代替 none
//不推荐
.foo {
  border: none;
}



//推荐
.foo {
  border: 0;
}
  1. Sass属性声明顺序
  • 首先列出除去 @include和嵌套选择器之外的所有属性声明。
  • 紧随后面的是 @include,这样可以使得整个选择器的可读性更高。
  • 如果有必要用到嵌套选择器,把它们放到最后,在规则声明和嵌套选择器之间要加上空白,相邻嵌套选择器之间也要加上空白。嵌套选择器中的内容也要遵循上述指引。

Sass @mixin 与 @include | 菜鸟教程

.btn-green {
  background: green;
  font-weight: bold;
  @include transition(background 0.5s ease);

  .icon {
    margin-right: 10px;
  }
}
  1. Saas 变量命名

变量名应使用破折号(例如 $my-variable)代替 camelCasedsnake_cased 风格。对于仅用在当前文件的变量,可以在变量名之前添加下划线前缀(例如 $_my-variable)。

  1. 嵌套选择器
  • 请不要让嵌套选择器的深度超过 3 层!
  • 永远不要嵌套 ID 选择器!

参考资料

前端性能优化原理与实践-小册

Web前端性能优化-田佳奇

渲染页面:浏览器的工作原理 - Web 性能 | MDN

渲染性能优化全局视角 - 掘金