由于在 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%">
!!!

!!!
</div>
!!!
[内容]
这肯定不是一个长久之计,如果我以后要将博客转移到其他平台将会是个大麻烦。
曲线解决办法
这两年断断续续的尝试过几次解决办法:
- 自定义代码块,PHP 替换
- 服务器后端返回图片宽高
- JS 获取图片宽高,生成图片
- 图片宽高信息写入文件名,通过正则匹配获取
迫于太麻烦/太菜等原因,最后都没用多久或者放弃。这两天又在思考,能不能从哪里找个突破口,简单的方式实现。最后把目光放到了 Markdown 上,Markdown 中引入一张图片的写法如下:

但一直以来 title
属性我都没用上,那么是否可以利用下,于是经过一天的探索终于优雅的解决了图片占位的问题。
首先是面临一个小问题,Typecho 本身用的 SegmentFault 自己编写的 Hyperdown 解析器,不知道是有 bug 还是我以前哪里魔改过,加上 title 内容后解析错误,输出 html 内容为:
<img src="/images/atpx.com.png" alt=" title=" title="" text""="">
<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
里写上图片的宽高百分比即可,如:

最后输出的 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)才开始支持,让我被迫冷静下来。但又仔细一想,先加上也无妨,每张图片都加上 width
和 height
属性也是件麻烦事,等以后图片多了之后工作量更大。
当问题不大,在推出 aspect-ratio
之前, 现代浏览器就又开始支持根据图像的 width
和 height
属性自动设置图像的默认长宽比了,并且兼容性会比 aspect-ratio
属性好一些。因此对于响应式图片来说,这里有两套解决方案:
- 设置
width
和height
属性再加上一点样式:
<img src="/images/atpx.com.png" width="1920" height="1080" style="max-width: 100%; height: auto;" />
- 设置
aspect-ratio
属性:
<img src="/images/atpx.com.png" style="aspect-ratio: 1920 / 1080; max-width: 100%;" />
所以我又按前面的方法又把所有图片的 Markdown 改成 width/height
的形式😣:

然后稍微改一下替换函数:
/**
* 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;
}
再次折腾下来的好处是可以少一层嵌套,并且战未来。随着时间的推移,等大多数人都升级祖传系统和浏览器之后,这套方案的体验会越来越好,也算是一劳永逸了。
如果你认为这篇文章还不错,可以考虑为我充电 ⚡️