CSS解决图片加载的布局移动问题
发布时间:
最后更新:
你有没有过在网速不好时,文章里加载图片后使文字下移,造成极差的阅读体验?比如下面这个网站:
图片加载造成布局移动
这浏览体验肯定是相当差,本站在开发时也遇到了这个问题,经一番实验后完美解决,特写本文记录这期间的思考历程。
直接看结果请跳到5,或者F12
控制台查看本文的图片元素。
分析下原因 #
HTML <img>和<video>(本站有用视频作为GIF的功能) 元素是一种可替换元素,当图片未加载时,它的内容是空白或者浏览器默认的内容,而图片加载后就会替换为图片。图片的大小肯定跟浏览器默认内容不一样,所以会造成img元素的尺寸改变,进而产生重新布局(Reflow)导致后面的文本移动。
基本方针 #
要解决此问题,就需要事先确定加图片载后img元素的大小,提前设置其width
和height
。这并不难,但是直接定死宽高会存在一个问题:如果图片比容器大怎么办?
对于这个问题,我们想要的是图片随着容器宽度的减小而缩放,当然图片的宽高比必须保持。能直接限制宽度不超出父元素的属性就是max-width: 100%
,但只使用它并不能解决此问题,因为它只管宽度不管高度,所以会这样:
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8">
<title>确定长宽比的堆叠</title>
<style>
main {
width: 310px;
padding: 20px;
border: dashed;
resize: horizontal;
overflow: auto;
}
img {
/* 图片的真实大小 960x540 */
width: 960px;
height: 540px;
max-width: 100%;
}
</style>
</head>
<body>
<!-- 图片未触发懒加载时没有src属性 -->
<main><img></main>
</body>
</html>
图片使用max-width: 100%
防止超出容器,可以看到当容器宽度小于图片时,img元素并未等比缩放而导致变形,高度保持在540px而不是等比缩放后的225px。所以仅靠这种简单地方式是不行的,必须要实现一个能固定宽高比的img。
有没有原生支持? #
很难想象这么一个有用的功能竟然没有一个通用的标准,如果仅限于某一种浏览器的话还是有些属性可以用的。我首先就搜到了intrinsicsize
这个属性,可惜兼容性完全不行。
紧接着我又尝试了给img元素加上尺寸,希望浏览器能聪明地用他们计算出比例
<style>
img {
max-width: 100%;
height: auto;
}
</style>
<!-- 图片未触发懒加载时没有src属性 -->
<img width="960" height="540">
很显然浏览器没我聪明,这种写法也毫无效果。
后来我又寄希望于CSS上,但发现既没有aspect-ratio
之类的属性,也没有height: calc(50% * computed-width)
这样的写法。
在剩下的查找中我渐渐放弃了使用原生支持的想法,没辙,只能想想其它的方式了。
用JS来处理? #
这种办法确实可行,比如这个博客的前端Aurora里就是这么做的。首先需要知道容器的宽度、图片的宽高,然后使用 if 跟四则运算即可轻松计算出合适的尺寸。
用JS实现的缺点在于除了图片的宽高外,还需要知道目标容器的宽度,这无疑增加了耦合,虽然通常不会导致什么问题。
对于一般人来说,到这就可以了。
但我是有强迫症的,能用CSS实现的我绝不在js文件里多打一行字!能不把渲染函数和容器元素耦合的我就绝不多加一个参数!能用更好的方法实现的我绝不会熟视无睹!
事实上这个需求是有纯CSS解决方案的,上面提到了img元素无法直接支持宽高比,但写程序一定不能死脑筋,这种时候就得用点骚套路,俗称HACK。
padding 实现宽高比 #
在这篇css-tricks的文章里提到了用padding
属性来实现宽高比,确实是一种好用的方法。
众所周知,padding
用来给元素设置内部填充,通常很少会在上下两边用到百分比值,左右填充使用百分比值相信大家都用过,这个百分比是按照父元素宽度来计算的,但事实上它在所有方向上都是这样(而不是上下方向使高度),使用padding-top: xx%
就可以让高度绑定到宽度上,实现宽高比。
可惜那篇文章里的写法是错的,大多数浏览器并不支持在img元素里加内容和伪元素,我不知道作者用得是什么浏览器,我试了Firefox和Chrome均无效。在这篇文章里说到目前只有Opera支持img里用伪元素。
但相信有经验的人此刻已经想到了解决方案:既然img不给用,那还可以把它套在父元素上啊,在父元素上实现宽高比,再把img大小设为跟父元素一致不就好了嘛。
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8">
<title>确定长宽比的堆叠</title>
<style>
:root {
--image-width: 640px;
--image-height: 360px;
--aspect-ratio: 56.25%; /* 16:9 */
}
main {
width: 400px;
padding: 20px;
border: dashed;
resize: horizontal;
overflow: auto;
}
.wrapper {
position: relative;
width: var(--image-width);
max-width: 100%;
margin: 0 auto;
}
.wrapper::before {
content: "";
display: block;
padding-top: var(--aspect-ratio);
}
.wrapper > * {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<script defer>
setTimeout(() => document.getElementById("img").src = "https://i.pximg.net/img-original/img/2017/11/01/21/49/07/65703510_p0.jpg", 4000)
</script>
</head>
<body>
<main>
<div class="wrapper"><img id="img"></div>
</main>
</body>
</html>
父元素.wrapper
上设置图片原始宽度并使用max-width
防止超出布局区域,内部的伪元素使用padding-top: <图片宽高比>
撑开高度即可,真正的图片则使用绝对定位堆叠在容器元素上,尺寸设置为跟容器一致。
下面是演示,拖动main元素的边框改变宽度,可以看到在图片宽度小于容器宽度时,img元素以固定的比例缩放。图片加载后元素大小也没有变化,不会导致文本布局改变,完美实现了最初的需求!
最终效果
本方案具有以下优势:
- 只需要图片宽高两个信息,拥有很低的耦合度,无需知晓目标容器
- 纯CSS实现性能非常好
- CSS属性都是些最常用的,可以兼容所有主流浏览器
其它 #
这篇CSS-Tricks文章的最后还提到了另一种方案,就是使用内联SVG作为图片元素的src
,通过指定SVG的viewBox
属性来动态设置宽高。这套路我还真没想到,而且确实可行,就是用SVG总感觉怪怪的。从灵活性上看,我这基于 dom + css 的实现比SVG可操作性更高。
我看到Medium网站也是用跟我同样的方案,不过它把padding
用在一个<div>里,然后把它作为图片的容器,效果一样,但我还是喜欢用伪元素来做这种功能。
最后发点牢骚,我费劲找到的方案有多大的价值?这不是什么精密的算法,也不是有创意的设计,正如一开始所说,浏览器就应该内置设置宽高比的功能,用HACK的方式来实现不过是填坑罢了。在未来浏览器支持后,这篇文章也许就一无是处了吧,唉,写前端就是这么多坑。