\n\n\n
\n\t
\n
\n\n\n```\n\n父元素`.wrapper`上设置图片原始宽度并使用`max-width`防止超出布局区域,内部的伪元素使用`padding-top: <图片宽高比>`撑开高度即可,真正的图片则使用绝对定位堆叠在容器元素上,尺寸设置为跟容器一致。\n\n下面是演示,拖动main元素的边框改变宽度,可以看到在图片宽度小于容器宽度时,img元素以固定的比例缩放。图片加载后元素大小也没有变化,不会导致文本布局改变,完美实现了最初的需求!\n\n@gif[最终效果](/video/NO_BxfvoH2tYYvPoJKTc.mp4?vw=782&vh=356)\n\n本方案具有以下优势:\n\n* 只需要图片宽高两个信息,拥有很低的耦合度,无需知晓目标容器\n* 纯CSS实现性能非常好\n* CSS属性都是些最常用的,可以兼容所有主流浏览器\n\n# 其它\n\n[这篇CSS-Tricks文章的最后](https://css-tricks.com/preventing-content-reflow-from-lazy-loaded-images/#we-have-a-winner)还提到了另一种方案,就是使用内联SVG作为图片元素的`src`,通过指定SVG的`viewBox`属性来动态设置宽高。这套路我还真没想到,而且确实可行,就是用SVG总感觉怪怪的。从灵活性上看,我这基于 dom + css 的实现比SVG可操作性更高。\n\n我看到[Medium网站](https://medium.com/)也是用跟我同样的方案,不过它把`padding`用在一个
里,然后把它作为图片的容器,效果一样,但我还是喜欢用伪元素来做这种功能。\n\n**最后发点牢骚,我费劲找到的方案有多大的价值**?这不是什么精密的算法,也不是有创意的设计,正如一开始所说,浏览器就应该内置设置宽高比的功能,用HACK的方式来实现不过是填坑罢了。在未来浏览器支持后,这篇文章也许就一无是处了吧,唉,写前端就是这么多坑。","create":1592569993931,"update":1674840680000,"viewCount":1547,"discussionCount":0,"banner":{"image":"/image/Rvo0zPOKPNXM5uoqGvSG.png","theme":0},"prev":{"id":1,"urlTitle":"enable-HTTP2-for-tomcat-with-lets-encrypt","title":"申请Let'sEncrypt证书,让Tomcat启用HTTP2.0"},"next":{"id":16,"urlTitle":"design-that-makes-me-uncomfortable-on-the-web","title":"网页里常见的一些糟糕的设计"}}}},"breakPoint":{"width":999999},"discussOptions":{"options":null},"HeadMetas":{"inherited":true,"hydration":0}}

CSS解决图片加载的布局移动问题

发布时间:

最后更新:

你有没有过在网速不好时,文章里加载图片后使文字下移,造成极差的阅读体验?比如下面这个网站:

图片加载造成布局移动

这浏览体验肯定是相当差,本站在开发时也遇到了这个问题,经一番实验后完美解决,特写本文记录这期间的思考历程。

直接看结果请跳到5,或者F12控制台查看本文的图片元素。

分析下原因 #

HTML <img>和<video>(本站有用视频作为GIF的功能) 元素是一种可替换元素,当图片未加载时,它的内容是空白或者浏览器默认的内容,而图片加载后就会替换为图片。图片的大小肯定跟浏览器默认内容不一样,所以会造成img元素的尺寸改变,进而产生重新布局(Reflow)导致后面的文本移动。

基本方针 #

要解决此问题,就需要事先确定加图片载后img元素的大小,提前设置其widthheight。这并不难,但是直接定死宽高会存在一个问题:如果图片比容器大怎么办

对于这个问题,我们想要的是图片随着容器宽度的减小而缩放,当然图片的宽高比必须保持。能直接限制宽度不超出父元素的属性就是max-width: 100%,但只使用它并不能解决此问题,因为它只管宽度不管高度,所以会这样:

html
<!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这个属性,可惜兼容性完全不行。

intrinsicsize 的兼容性intrinsicsize 的兼容性

紧接着我又尝试了给img元素加上尺寸,希望浏览器能聪明地用他们计算出比例

html
<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大小设为跟父元素一致不就好了嘛。

html
<!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的方式来实现不过是填坑罢了。在未来浏览器支持后,这篇文章也许就一无是处了吧,唉,写前端就是这么多坑。

评论加载中