滚动锚定(Scroll Anchoring)- 让视口内容不再因视口上方 DOM 元素的高度变化而产生跳动

不知道你有没有经历过这样的场景:当你打开一张“多图杀猫”的页面后,正一张图一张图边滚边看,在你刚准备定睛看某一张图的时候,这张图突然被它上面的内容挤到了视口下方,然后你赶紧把滚动条往下拉,试图追赶这张没看完的图,当你刚刚追上的时候,这张图又一次被挤到了你看不见的地方。

发生这种情况的原因是因为在很多场景下(比如论坛里),你没法事先知道一张图的高度,所以你没法事先给这张图占位,在网速不理想的情况下,可能就会发生我上面描述的这种因页面靠上的图片比靠下的图片晚加载出来而导致用户当前浏览的内容被频繁挤出视口的情况。

我通过在定时器回调里向页面上方插入图片来模拟一下刚才描述的这种情况:

<style>
  img {
    display: block;
    margin: 0 auto;
  }
</style>
<img src="https://aecpm.alicdn.com/tfscom/TB1.52aPFXXXXa0XXXXXXXXXXXX.jpg">
<img src="https://aecpm.alicdn.com/tfscom/TB1_utRPVXXXXapXVXXXXXXXXXX.png">
<img src="https://static.dingtalk.com/media/lAHOuOFd_czSzQEn_295_210.gif">
<img src="https://aecpm.alicdn.com/tfscom/TB1f1xwQpXXXXXBXVXXXXXXXXXX.jpg">
<img src="https://gtms03.alicdn.com/tps/i3/TB1eSxvJVXXXXaKXFXXYoAvIXXX-220-50.png">
<img src="https://gw.alicdn.com/bao/uploaded/TB1EGvvPVXXXXX3aXXXXXXXXXXX-200-200.jpg">
<img src="https://gw.alicdn.com/tfscom/TB1CLTHNFXXXXaDXpXXXXXXXXXX">
<script>
  const urls = `
https://asearch.alicdn.com/bao/uploaded/i1/1381306006414474986/TB2_gZAlNtmpuFjSZFqXXbHFpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i1/153360285303496277/TB2SO.Wa4vzQeBjSZFEXXbYEpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i1/188050339412916381/TB2geTXaypnpuFjSZFkXXc4ZpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i2/181720289489216985/TB2UFz6amjz11Bjy0FnXXcnxXXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/108480250457898935/TB28r5osFXXXXbrXXXXXXXXXXXX_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/111180208599309441/TB2kAsQnVXXXXXcXFXXXXXXXXXX_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/171530328819399773/TB2rgtke9iK.eBjSZFsXXbxZpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/1880505035634435666/TB2bToNiHXlpuFjSszfXXcSGXXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i4/1519305020726924733/TB2I2VuhNhmpuFjSZFyXXcLdFXa_!!0-saturn_solar.jpg
`.split("\n")
  let i = 0
  setInterval(() => {
    if (i === urls.length) i = 0
    let img = new Image()
    img.src = urls[i++]
    document.body.prepend(img)
  }, 2000)
  onscroll = function(argument) {
    console.log("scrollY:" + scrollY)
  }
</script>

上面这个 demo 里,假设我一直“追赶”的那张图是“金凯瑞摇头三人组”那张 GIF,那么在 Chrome 56 之前的版本以及在其它的浏览器中,你看到的会是下面这样的场景:

为了获得更好的用户体验,Chrome 从 56 开始,开启了一个叫做“滚动锚定(Scroll Anchoring)”的优化,效果就是,当页面在视口上方的部分突然变高了 x 像素,那么浏览器会为你自动向下滚动 x 像素,从而保证视口内容完全不变:

浏览器自动为你向下滚动 x 像素,就意味着浏览器自己会触发一次 scroll 事件,也意味着 scrollY 的值会增加 x,你可以通过上面的 demo 验证这一点。

有些同学可能会有疑问,“这种场景多吗?”、“我怎么从来没注意到?”、“有必要把事情搞复杂吗?”。 从 Chrome 官方的统计可以看到,这个特性被触发(替你滚动页面)的概率大概为 1%,并不多,但也算不上是极端情况,所以优化还是有必要的。可能因为近些年网络条件越来越好,图片加载的速度比你滚动页面的速度还要快,所以不太容易遇到因网速慢导致的这类场景了(尤其在 WIFI 网络下)。

不过这个优化的确不是个简单的改动,Chrome 从去年 3 月份开始实现这个特性,直到一年多后的今天,仍然有一些因这个优化导致的 bug 存在,这些 bug 多表现为页面异常滚动,甚至像永动机一样无限抖动,从这方面看,事情的确有一些被搞的复杂了。但幸好有一个 CSS 属性可以关掉这个优化:overflow-anchor: none,你可以把这个属性添加到发生 bug 的容器元素上,甚至加到 body 元素上也行,然后该元素及其它的所有后代节点就都不会被应用“滚动锚定”的优化了。除了作为浏览器 bug 的临时 fix,我想不到其它使用这个属性的场景了。

这个优化不仅限于看图片的时候,任何元素节点,甚至文本节点也同样适用。比如你在某新闻网站浏览一段文字的时候,视口上方突然异步插入了一个未事先占位的 iframe 广告(微博输入框下方就有这么一个广告),如果你使用了 Chrome 56 及以上版本的话,你完全察觉不到这一变化,你的阅读不会被打断。

页面在视口上方的高度增加 x 像素,浏览器会为你向下滚动 x 像素;反过来,页面在视口上方的高度减少 x 像素,浏览器也会为你向上滚动 x 像素,但这种情况更少见了。

该优化同样适用于元素级别的滚动条,我也写了一个 demo:

<style>
  div {
    width: 300px;
    height: 300px;
  }

  #container {
    background: red;
    overflow: scroll;
  }

  #aboveViewport {
    background: blue;
  }

  #anchorNode {
    background: green;
  }
</style>
<div id="container">
  向下滚动到底
  <div id="aboveAnchorNode"></div>
  <div id="anchorNode"></div>
  这段文字一旦出现就会始终在视口内
</div>
<script>
  let height = 100
  setInterval(() => {
    aboveAnchorNode.style.height = height += 10
  }, 1000)
</script>

由于本文讲的是一个浏览器的优化,即便是前端开发者也没有深究的必要,所以我故意省略了一些内容,比如什么是锚定节点(anchor node )以及浏览器如何选定一个锚点节点?以及哪些样式改动会把锚定节点挤出视口但不会触发优化(Suppression Triggers),如果你想深究,可以从规范里找到答案。

时间: 2024-08-28 00:14:44

滚动锚定(Scroll Anchoring)- 让视口内容不再因视口上方 DOM 元素的高度变化而产生跳动的相关文章

深度稿件:4K电视将爆发 内容不再是拦路虎

笔者按:对于一名极客而言,笔者往往是新鲜科技产品"身先力行"的体验者.但说实话,也总会对某些新玩意儿避而远之.比如4K电视已经炒作了快一年的时间,但笔者依然没有动力去将家中的智能电视更新一下.不过就现在4K电视的发展来看,笔者似乎应该更新换代了. 4K电视的技术参数之类的就不再一一阐述,不明白的不妨先去百度一番--总之,4K就是让你能通过更清晰地屏幕认知这个花花世界.虽然4K电视上市已经有一年左右的时间,但提起4K电视人们还总是用老眼光去看待它:价格昂贵.节目资源少.暴利.厂商噱头.无

JS复制DOM元素文字内容

要实现的效果:将HTML页面中的某个DOM元素例如DIV下面的文本内容进行复制. 实现过程如下: 1 <html> 2 <head> 3 <title>Copy text Demo</title> 4 <script type="text/javascript" src="jquery.min.js"></script> 5 <script type="text/javascri

页面滚动到一定位置时才显示在指定位置上的元素的jquery代码

当前可视窗口的顶部到页面的顶部高度+可视页面的高度>元素的绝对高度+元素自身高度时,显示当前元素. 页面滚动到一定位置时才显示在指定位置上的元素! 将$(".timeline.animated .timeline-row")换成指定的选择器即可! (function() {  $(document).ready(function() {    var timelineAnimate;    timelineAnimate = function(elem) {      retur

js对元素属性.内容的操作。定时器。元素的平级,父级,子集关系。

JS对元素内容的操作: 1.操作样式(style) 2.操作属性(Attribute) 3.操作内容(innerHtml/Text) 一.操作元素属性 常见属性:class.id.style.disabled. 自定义属性:可以自定义一个任意属性名称,可以任意放值,不会对所在元素本身有任何作用. this:代表所在最近的一个方法. 设置一个属性:a.setAttribute("属性名称","属性值");[不存在该属性名称时,创建这个属性,并赋值.当存在该属性名称时,

2017-3-31 js对元素属性.内容的操作。定时器。元素的平级,父级,子集关系。

JS对元素内容的操作: 1.操作样式(style) 2.操作属性(Attribute) 3.操作内容(innerHtml/Text) 一.操作元素属性 常见属性:class.id.style.disabled. 自定义属性:可以自定义一个任意属性名称,可以任意放值,不会对所在元素本身有任何作用. this:代表所在最近的一个方法. 设置一个属性:a.setAttribute("属性名称","属性值");[不存在该属性名称时,创建这个属性,并赋值.当存在该属性名称时,

[ jquery 文档处理 prepend(content|fn) ] 此方法用于向每个匹配的元素内部前置内容,这是向所有匹配元素内部的开始处插入内容的最佳方式

向每个匹配的元素内部前置内容,这是向所有匹配元素内部的开始处插入内容的最佳方式 实例: <html lang='zh-cn'> <head> <title>Insert you title</title> <meta http-equiv='description' content='this is my page'> <meta http-equiv='keywords' content='keyword1,keyword2,keywor

jquery scroll 自动加载内容

当拖动滚动条时,自动加载内容 </pre><pre name="code" class="javascript">1. 首先计算li的总数 </pre><pre code_snippet_id="683281" snippet_file_name="blog_20150603_5_4812406" name="code" class="javascript

jquery 文字滚动大全 scroll 支持文字或图片 单行滚动 多行滚动 带按钮控制滚动

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-

js 滚动加载iframe框中内容

var isIE6 = !!window.ActiveXObject&&!window.XMLHttpRequest; //滚动加载 var scrollLoad =function(){ $("#content iframe[_src]").each(function(){ var t = $(this); if( t.offset().top<= $(document).scrollTop() + $(window).height() ) { t.attr(