文字尾部展开按钮解决方案一览
前言
这是之前做需求时遇到的一个问题,抖音 semi ui Typography 下的 Paragraph 组件在被大量使用时产生了性能问题,具体场景是在列表使用时发生的。为了解决这个问题将组件改成了自己的实现,同时把列表换成了虚拟列表。
这次优化是虚拟列表立了大功,但是个人在探索了 semi ui 的方案后发现了门道也有不少,于是打算做个总结
css 实现
这种方式比较简单,参考文章:https://juejin.cn/post/6963904955262435336#comment
这里简单讲解一下思路:
- 使用浮动让展开按钮悬浮起来,并通过 float: right; 将其移动到右侧
- 使用另一个隐藏元素,同样浮动到右侧,并且把高度设置为隐藏时元素的高度 - 按钮的高度
- 对按钮清除浮动(浮动的作用就是用来移动到上一个浮动元素的下面)
- 当文字行数不够时,在文字尾部用一个足够长宽的元素挡住展开按钮 https://juejin.cn/post/6963904955262435336#heading-6
方案的优缺点:
- 纯 css 计算,性能会比 js 要好
- 使用到相对现代的 css 实现 (calc, -webkit-box, line-clamp) 目前主流浏览器已全部兼容
- 默认浮动到右下角,如果样式需要精细调整会有点麻烦
js 实现
这种方式适合兼容老式浏览器,因为不需要特殊 css 处理
个人的思路如下:
- 先测量当前文字是否超出容器指定文字行数
- 是,进入以下步骤
- 采用二分法截断文字,直到测出截断后文字能够同时塞入结尾省略内容 + 展开按钮
- 否,直接展示文字
- 是,进入以下步骤
semi ui 为什么会存在性能瓶颈
原因
- 源码实现里在全局放置了一个公用 dom,专门用来测量文字高度
- 当页面上有多个组件一次性渲染出来时,就需要多次替换文字 + 父元素样式进行测量,间接导致触发多次重排
- 其中一次测量本身就会有多次重排:
- 获取容器原样式替换到测量容器时会有一次重排重绘
- 测量文字截断到哪里才能容下所有自定义元素(ologn),每一次截断计算也都要重排
所以页面阻塞的时间长度为 onlogn n 为平均重排占用时长
优化?
除了 canvas,核心思路都是推迟/减少一次性计算量来避免页面卡顿
canvas 测量文字
可以说效果显著,平均一次测量的时间下降到了微秒级别,但是有几个问题:
- 文字截断截少了
- 这里采用的逻辑是测量出文字在 canvas 上渲染后的长度,并且用原元素宽度将计算宽度相除得到的占用行数,问题就出在这里,dom 的文字排版中,假如一行剩余的空间塞不下一个字,就排到下一行,而这里的计算没有考虑这种情况,就相当于我把一个字截断了塞到了剩下的空间,再把截断后剩下的文字排到第二行(最大限度利用空间说是)。这就导致了计算出的文字数量比通过 dom 计算的要多
- 解决办法是遍历文字,并计算出到哪个文字就会发生一行内空间不够的情况,并将这个宽度记录累加到一个记录上,再最后计算占用行数时,把这个累加值也加上,就能得到真实占用行数(即模拟排版系统的计算过程)(真好奇 chrome 的排版系统是怎么做到高效计算的喵)
- 自己的实现是通过
getComputedStyle
获取原 element font 属性,这里依然会触发一次回流,如果从 element.style 获取的话是只能拿到通过 style 设置的样式- 大量渲染的场景一般来说不可能会有每一样 item 样式都一致的情况,可以考虑基于此给每个 item 做样式分类,以避免重复计算 font style
用 intersection observer
这种优化思路主要是避免不可见区域的计算,可以预见的缺点:
- 该观察器的任务优先级很低,回调会等到浏览器空闲时才执行,因此有可能出现页面渲染出来了但是没有进行文字缩略的计算
中断计算
类似 react 中断,但是这个涉及到 dom,由于可能会影响用户体验所以不建议使用
对于组件优化原文的思考
scrollWidth 属性获取缓存
这个实测出来浏览器有缓存,多次获取不会重排,但是实际情况可能得再看看
fragment 减少重绘
这种方法我觉得只能降低重绘时间,开发的成本可能会更高,因为存在样式隔离的问题,通过 getComputedStyle 获取样式的开销是不会少的
参考文献
https://bytedance.larkoffice.com/docx/AHcRdimGtohQ64xVW4PcJsqDnKg
个人信息

Shiinafan
文章
47
标签
53
归档
4