由于在 Web 中图片是在 DOM 结构渲染完成以后调用的,如果网速较慢,在 DOM 结构渲染完成时图片尚未加载,此时图片的高度会默认为 0;当图片加载完成后,会重新计算进行重绘,所以会出现抖动现象(图片下面的内容被挤下去)。Google 也针对这种页面布局抖动提出了累积布局偏移(Cumulative Layout Shift)这一指标用来测量真实用户体验中发生偏移的频率。

虽然对内容没什么影响,但对用户体验来说还是比较差的,特别是网络比较差的情况下。

以前用的解决办法

为了解决这个问题,以前一直都在用一个笨办法,直接在图片外加一个 wrapper,利用 CSS 的特性,使用 padding-bottom 来占位。举一个例子:

<div class="img-wrapper">
  <img src="/atpx.com/img.png">
</div>
.img-wrapper {
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 56.25%; 
}

img {
  position: absolute;
  top: 0;
  left: 0;
  max-width: 100%;
}

简单来说就是提前给图片一个父元素 <div>,设置 padding-bottom,计算图片的 宽/高*100%,若图片为 16:9,则 padding-bottom 的值为 56.25%. 由于 CSS 是在 DOM 结构渲染时就已经排好版了,所以后续加载的图片只需要在父元素里伸展躺平就行了。

这样写的好处就是简单、兼容性高,缺点也显而易见,如果图片的大小不一样,每张图片都要单独设置。我还真就每张图片这样设置 🙃,因为 Typecho 的 Markdown 中支持引入 HTML 语句,所以过去两年我的每篇文章就像下面这样:

[内容]

!!!
<div class="img-wrapper" style="padding-bottom:23.33%">
!!!
![example](/images/atpx.com.png)
!!!
</div>
!!!

[内容]

这肯定不是一个长久之计,如果我以后要将博客转移到其他平台将会是个大麻烦。

曲线解决办法

这两年断断续续的尝试过几次解决办法:

  • 自定义代码块,PHP 替换
  • 服务器后端返回图片宽高
  • JS 获取图片宽高,生成图片
  • 图片宽高信息写入文件名,通过正则匹配获取

迫于太麻烦/太菜等原因,最后都没用多久或者放弃。这两天又在思考,能不能从哪里找个突破口,简单的方式实现。最后把目光放到了 Markdown 上,Markdown 中引入一张图片的写法如下:

![alt text](/images/atpx.com.png "title text")

但一直以来 title 属性我都没用上,那么是否可以利用下,于是经过一天的探索终于优雅的解决了图片占位的问题。

首先是面临一个小问题,Typecho 本身用的 SegmentFault 自己编写的 Hyperdown 解析器,不知道是有 bug 还是我以前哪里魔改过,加上 title 内容后解析错误,输出 html 内容为:

<img src="/images/atpx.com.png" alt=" title=" title="" text""="">

通过插件换成 Parsedown 解析器后正常解析:

<img src="/images/atpx.com.png" alt="alt text" title="title text">

接下来的操作思路就很清晰了,首先在主题 function.php 文件中添加一个函数用来加上 img-wrapper 并替换 padding-bottom 的值为 title 的值:

/**
 * Stop html page shakes
 * Written by ATP on 2022-10-07
 * Website: https://atpx.com
 */
function getContent($content) {
	$pattern = '/\<img.*?src\=\"(.*?)\".*?alt\=\"(.*?)\".*?title\=\"(.*?)\"[^>]*>/i';
	$replacement = '<div class="img-wrapper" style="padding-bottom:$3%"><img src="$1" alt="$2"></div>';
	$content = preg_replace($pattern, $replacement, $content);
	return $content;
}

甚至可以顺便加入懒加载的属性,目前这个属性浏览器的支持也很不错了:

<img src="$1" alt="$2" loading="lazy"> 

然后在输出文章内容的 post.php 文件中找到 <?php $this->content(); ?>,替换为:

<?php echo getContent($this->content); ?>

之后只需要在写文章插入图片时在 title 里写上图片的宽高百分比即可,如:

![alt text](/images/atpx.com.png "56.25")

最后输出的 html 如下:

<div class="img-wrapper" style="padding-bottom:56.25%">
    <img src="/images/atpx.com.png" alt="alt text">
</div>

虽然并不是自动化的解决问题,但目前的方式我还是比较满意了,也能在提升一点写作效率的同时保证 Markdown 文章的多平台一致性。

Updated on 2023-02-13

今天又看了一遍谷歌的文章 优化 Cumulative Layout Shift 累积布局偏移,似乎又有新花样—— aspect-ratio 属性,给容器设置一个宽高比,可以极大的方便布局和优化 CLS。

有了aspect-ratio,你只需要给一个宽度或者高度,它就能自己计算出另一个值,也就是说我只需要给每个 <img> 一个宽高比 aspect-ratio 和宽度 width,图片容器就可以自动计算占位了,可以省去一层嵌套。

正当我撸起袖子准备开干的时候,看了一眼兼容性,可惜的是从 Chrome 88(2021-01-19)才开始支持,而 Safari 等到 15( 2021-09-20)才开始支持,让我被迫冷静下来。但又仔细一想,先加上也无妨,每张图片都加上 widthheight 属性也是件麻烦事,等以后图片多了之后工作量更大。

当问题不大,在推出 aspect-ratio 之前, 现代浏览器就又开始支持根据图像的 widthheight 属性自动设置图像的默认长宽比了,并且兼容性会比 aspect-ratio 属性好一些。因此对于响应式图片来说,这里有两套解决方案:

  1. 设置 widthheight 属性再加上一点样式:
<img src="/images/atpx.com.png" width="1920" height="1080" style="max-width: 100%; height: auto;" />
  1. 设置 aspect-ratio 属性:
<img src="/images/atpx.com.png" style="aspect-ratio: 1920 / 1080; max-width: 100%;" />

所以我又按前面的方法又把所有图片的 Markdown 改成 width/height 的形式😣:

![alt text](/images/atpx.com.png "1920/1080")

然后稍微改一下替换函数:

/**
 * Stop html page shakes
 * Updated by ATP on 2023-02-13
 * website: https://atpx.com
 */
function getContent($content) {
	$pattern = '/\<img.*?src\=\"(.*?)\".*?alt\=\"(.*?)\".*?title\=\"(.*?)\/(.*?)\"[^>]*>/i';
	$replacement = '<img style="aspect-ratio:$3/$4" src="$1" width="$3" alt="$2" loading="lazy">';
	$content = preg_replace($pattern, $replacement, $content);
	return $content;
}

再次折腾下来的好处是可以少一层嵌套,并且战未来。随着时间的推移,等大多数人都升级祖传系统和浏览器之后,这套方案的体验会越来越好,也算是一劳永逸了。

如果你认为这篇文章还不错,可以考虑为我充电 ⚡️