【第1159期】CSS预加载Preload
前言
看天气预报,今天好多地方都开始下雪了。今日早读文章由@李斌分享。
正文从这开始~
Preload 作为一个新的web标准,旨在提高性能和为web开发人员提供更细粒度的加载控制。Preload使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。
在 HTML 代码中,它看上去大概是下面这样的一段声明式获取指令(declaratiev fetch directive)。
<link rel=“preload”>
拿我们的话来说,通过这一方法我们告诉浏览器开始获取某一特定资源,毕竟我们是作者,知道浏览器很快就会用到这一资源。
与现有的类似技术的区别
事实上,关于预加载,我们已经有<link rel=“prefetch”>
,而且浏览器支持情况还不错。
除此之外,Chrome还支持过<link rel=“subresource”>
。
但Preload与这两者不同,<link rel=“prefetch”>
的作用是告诉浏览器加载下一页面可能会用到的资源,注意,是下一页面,而不是当前页面。因此该方法的加载优先级非常低(自然,相比当前页面所需的资源,未来可能会用到的资源就没那么重要了),也就是说该方式的作用是加速下一个页面的加载速度。
<link rel=“subresource”>
的设计初衷是处理当前页面,但最后还是壮烈牺牲了。因为开发者无法控制资源的加载优先级,因此浏览器(其实也只有 Chrome 和基于 Chrome 的浏览器)在处理此类标签时,优先级很低,到底有多低呢?这么说吧,在大多数情况下,用了等于没用。
为什么 Preload 更好
Preload是为处理当前页面所生,这点和 subresource 一样,但他们之间有着细微且意义重大的区别。Preload 有 as 属性,这让浏览器可做一些 subresource 和 prefetch 无法实现的事:
浏览器可以设置正确的资源加载优先级,这种方式可以确保资源根据其重要性依次加载, 所以,Preload既不会影响重要资源的加载,又不会让次要资源影响自身的加载。
浏览器可以确保请求是符合内容安全策略的,比如,如果我们的安全策略是Content-Security-Policy: script-src 'self'
,只允许浏览器执行自家服务器的脚本,as 值为 script 的外部服务器资源就不会被加载。
浏览器能根据 as 的值发送适当的 Accept 头部信息
浏览器通过 as 值能得知资源类型,因此当获取的资源相同时,浏览器能够判断前面获取的资源是否能重用。
Preload 的与众不同还体现在 onload 事件上(至少在 Chrome 中,prefetch 和 subresource 是不支持的)。也就是说你可以定义资源加载完毕后的回调函数。
<link rel="preload" href="..." as="..." onload="preloadFinished()">
此外,preload 不会阻塞 windows 的 onload 事件,除非,preload资源的请求刚好来自于会阻塞 window 加载的资源。
结合上面所有这些特征,preload 给我们带来了一些以前不可能实现的功能。
资源的提前加载:
preload 一个基本的用法是提前加载资源,尽管大多数基于标记语言的资源能被浏览器的预加载器(Preloader)尽早发现,但不是所有的资源都是基于标记语言的,比如一些隐藏在 CSS 和 Javascript 中的资源。当浏览器发现自己需要这些资源时已经为时已晚,所以大多数情况,这些资源的加载都会对页面渲染造成延迟。
Preloader 简介
HTML 解析器在创建 DOM 时如果碰上同步脚本(synchronous script),解析器会停止创建 DOM,转而去执行脚本。所以,如果资源的获取只发生在解析器创建 DOM时,同步脚本的介入将使网络处于空置状态,尤其是对外部脚本资源来说,当然,页面内的脚本有时也会导致延迟。
预加载器(Preloader)的出现就是为了优化这个过程,预加载器通过分析浏览器对 HTML 文档的早期解析结果(这一阶段叫做“令牌化(tokenization)”),找到可能包含资源的标签(tag),并将这些资源的 URL 收集起来。令牌化阶段的输出将会送到真正的 HTML 解析器手中,而收集起来的资源 URLs 会和资源类型一起被送到读取器(fetcher)手中,读取器会根据这些资源对页面加载速度的影响进行有次序地加载。
现在,有了 preload,你可以通过一段类似下面的代码对浏览器说,”嗨,浏览器!这个资源你后面会用到,现在就加载它吧。“
<link rel="preload" href="late_discovered_thing.js" as="script">
as 属性的作用是告诉浏览器被加载的是什么资源,可能的 as 值包括:
“script”
“style”
“image”
“media”
“document”
更多请参考fetch spec
忽略 as 属性,或者错误的 as 属性会使 preload 等同于 XHR 请求,浏览器不知道加载的是什么,因此会赋予此类资源非常低的加载优先级。
对字体的提前加载
web 字体是较晚才能被发现的关键资源(late-discovered critical resources)中常见的一类 。web 字体对页面文字的渲染资至关重要,但却被深埋 CSS 中,即便是预加载器有解析 CSS,也无法确定包含字体信息的选择器是否会真正应用在 DOM 节点上。理论上,这个问题可以被解决,但实际情况是没有一个浏览器解决了这个问题。而且,即便是问题得到了解决,浏览器能对字体文件做出合理的预加载,一旦有新的 css 规则覆盖了现有字体规则,前面的预加载就多余了。
总之,非常复杂。
但有了 preload 这个标准,简单的一段代码就能搞定字体的预加载。
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
需要注意的一点是:crossorigin 属性是必须的,即便是字体资源在自家服务器上,因为用户代理必须采用匿名模式来获取字体资源。
type 属性可以确保浏览器只获取自己支持的资源。尽管Chrome 支持 WOFF2,也是目前唯一支持 preload 的浏览器,但未来或许会有更多的浏览器支持 preload,而这些浏览器支不支持 WOFF2 就不好说了。
动态加载,但不执行
另外一个有意思的场景也因为 preload 的出现变得可能——当你想加载某一资源但却不想执行它。比如说,你想在页面生命周期的某一时刻执行一段脚本,而你无法对这段脚本做任何修改,不可能为它创建一个所谓的 runNow()函数。
在 preload 出现之前,你能做的很有限。如果你的方法是在希望脚本执行的位置插入脚本,由于脚本只有在加载完成以后才能被浏览器执行,也就是说你得等上一会儿。如果采用 XHR 提前加载脚本,浏览器会拒绝重用这段脚本,有些情况下,你可以使用 eval 函数来执行这段脚本,但该方法并不总是行得通,也不是完全没有副作用。
现在有了 preload,一切变得可能
var link = document.createElement("link");link.href = "myscript.js";link.rel = "preload";link.as = "script";document.head.appendChild(link);
上面这段代码可以让你预先加载脚本,下面这段代码可以让脚本执行
var script = document.createElement("script");script.src = "myscript.js";document.body.appendChild(script);
基于标记语言的异步加载
先看代码
<link rel="preload" as="style" href="asyncstyle.css" onload="this.rel='stylesheet'">
preload 的 onload 事件可以在资源加载完成后修改 rel 属性,从而实现非常酷的异步资源加载。
脚本也可以采用这种方法实现异步加载
难道我们不是已经有了
<script async>
虽好,但却会阻塞 window 的 onload 事件。某些情况下,你可能希望这样,但总有一些情况你不希望阻塞 window 的 onload 。
举个例子,你想尽可能快的加载一段统计页面访问量的代码,但又不愿意这段代码的加载给页面渲染造成延迟从而影响用户体验,关键是,你不想延迟 window 的 onload 事件。
有了preload, 分分钟搞定。
<link rel="preload" as="script" href="async_script.js" onload="var script = document.createElement('script'); script.src = this.href; document.body.appendChild(script);">
响应式加载
preload 是一个link,根据规范有一个media 属性(现在 Chrome 还不支持,不过快了),该属性使得选择性加载成为可能。
有什么用处呢?假设你的站点同时支持桌面和移动端的访问,在使用桌面浏览器访问时,你希望呈现一张可交互的大地图,而在移动端,一张较小的静态地图就足够了。
你肯定不想同时加载两个资源,现在常见的做法是通过 JS 判断当前浏览器类型动态地加载资源,但这样一来,浏览器的预加载器就无法及时发现他们,可能耽误加载时机,影响用户体验和 SpeedIndex 评分。
怎样才能让浏览器尽可能早的发现这些资源呢?还是 Preload!
通过 Preload,我们可以提前加载资源,利用 media 属性,浏览器只会加载需要的资源。
<link rel="preload" as="image" href="map.png" media="(max-width: 600px)"><link rel="preload" as="script" href="map.js" media="(min-width: 601px)">
HTTP 头
Preload 还有一个特性是其可以通过 HTTP 头信息被呈现。也就是说上文中大多数的基于标记语言的声明可以通过 HTTP 响应头实现。(唯一的例外是有 onload 事件的例子,我们不可能在 HTTP 头信息中定义事件处理函数。)
Link: <thing_to_load.js>;rel="preload";as="script"Link: <thing_to_load.woff2>;rel="preload";as="font";crossorigin
这一方式在有些场景尤其有用,比如,当负责优化的人员与页面开发人员不是同一人时(也就是说优化人员可能无法或者不想修改页面代码),还有一个杰出的例子是外部优化引擎(External optimization engine),该引擎对内容进行扫描并优化。
特征检查 (Feature Detection)
前面所有的列子都基于一种假设——浏览器一定程度上支持 preload,至少实现了脚本和样式加载等基本功能。但如果这个假设不成立了。一切都将是然并卵。
为了判断浏览器是否支持 preload,我们修改了 DOM 的规范从而能够获知 rel 支持那些值(是否支持 rel=‘preload’)。
至于如何进行检查,原文中没有,但 Github有一段代码可供参考。
var DOMTokenListSupports = function(tokenList, token) { if (!tokenList || !tokenList.supports) { return; } try { return tokenList.supports(token); } catch (e) { if (e instanceof TypeError) { console.log("The DOMTokenList doesn't have a supported tokens list"); } else { console.error("That shouldn't have happened"); } }};var linkSupportsPreload = DOMTokenListSupports(document.createElement("link").relList, "preload");if (!linkSupportsPreload) { // Dynamically load the things that relied on preload.}
讨论地址:https://github.com/w3c/preload/issues/7
是否可以用 HTTP/2 Push 完成 preload 的工作?
当然不行,尽管有一些相同的特性,但总的来说,他们的关系是互补而不是取代。
HTTP/2 Push 的优势是能够主动推送资源给浏览器,也就是说,服务器甚至不需要等到资源请求就能将资源推送给浏览器。
而 Preload 的优势在于其加载过程是透明的,一旦资源加载完毕或出现异常,应用可以获得事件通知。这一点是 HTTP/2 Push所不具备的。另外,Preload 还能加载第三方资源,但 HTTP/2 Push 不能。
此外,HTTP/2 Push 没办法将浏览器的缓存和非全局 cookie (non-global cookie) 考虑进去。也就是说,服务器推送的内容可能已经存在于客户端的缓存中,从而导致毫无意义的网络传输。(不过一份新的规范旨在解决该问题——cache digest specification,Github 上的 一个轻量级 Web服务H2O器实现了该功能,H2O在1.5版中引入了基于cookie 的cache-aware server push,原理是在首次 Server Push 完成后,在客户端存一个指纹,服务端后续检查到指纹存在时,先在指纹中查询要 Push 的资源,没查到才推送),但是非全局的 cookie就没这么好运了。对于这类型的资源,Preload 才是你的朋友。
Preload还有一个HTTP/2 Push 所不具备的能力是可以进行内容协商(content negotiation),也就是说如果你想通过 Client-Hints或者 HTTP 头的 accept 信息获取最合适的资源格式,HTTP/2 Push 帮不了你。
优化核心依旧是减少下载时间
JS篇中的预先解析DNS(dns-prefetch)依旧适用,提前解析CSS文件所在域名的DNS。
Preload
因为CSS已经在head中,我们不需要为css加preload属性了,但是css中用到的字体文件,一定要在所有css之前proload上。
<link rel="preload" href="/webfont.woff2" as="font">
首页CSS内联,非必要CSS异步加载
首页用到的CSS内联写在<head>
中,其余CSS均采用异步加载,可以采用这种自己实现的加载CSS的方法,在合适的需要时加载需要的css
function LoadStyle(url) { try { document.createStyleSheet(url) } catch(e) { var cssLink = document.createElement('link'); cssLink.rel = 'stylesheet'; cssLink.type = 'text/css'; cssLink.href = url; var head = document.getElementsByTagName('head')[0]; head.appendChild(cssLink) }}
如果你使用webpack,那就更轻松了,使用import函数,大致如下
// 在a.js模块中直接引入cssimport 'style.css'// 在需要a.js模块的地方improt('path-of-a.js').then(module => {})
webpack打包后,其实是把style.css打包进了a.js,在异步加载a.js的时候,会将style.css中的代码插入haed
标签中。
终极完美结构
<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>Faster</title> <link rel="dns-prefetch" href="//cdn.cn/"><link rel="preload" href="//cdn.cn/webfont.woff2" as="font"> <link rel="preload" href="//cdn.cn/Page1-A.js" as="script"> <link rel="preload" href="//cdn.cn/Page1-B.js" as="script"><link rel="prefetch" href="//cdn.cn/Page2.js"> <link rel="prefetch" href="//cdn.cn/Page3.js"> <link rel="prefetch" href="//cdn.cn/Page4.js"><style type="text/css"> /* 首页用到的CSS内联 */ </style></head><body><script type="text/javascript" src="//cdn.cn/Page1-A.js" defer></script><script type="text/javascript" src="//cdn.cn/Page1-B.js" defer></script></body></html>
最后,为你推荐
【第1053期】Preload,Prefetch 和它们在 Chrome 之中的优先级
关于本文
作者:@李斌
原文:https://zhuanlan.zhihu.com/p/32561606
【第1155期】如何快速融入新团队?
【第1151期】技术的热门度曲线
【第1159期】CSS预加载Preload相关推荐
- 好看的css预加载旋转动画 与 流光字体
今天刚好在做这个功能,就实现一个 预加载的动画效果,随手记录 一.预加载旋转动画 Html <view class="concentric_round"></vi ...
- prefetch 和preload_资源预加载preload和资源预读取prefetch简明学习
前面的话 基于VUE的前端小站改造成SSR服务器端渲染后,HTML文档会自动使用preload和prefetch来预加载所需资源,本文将详细介绍preload和prefetch的使用 资源优先级 在介 ...
- GORM v2 关联预加载Preload和Joins的区别
前言 本文中使用到的数据表结构以及GORM版本的区分详见以下文章:GORM v2 一对一关联查询使用(Belongs To .Has One) 执行区别 调用gorm的Debug方法打印一下一对一关联 ...
- android调用h5预加载图片,图片预加载 preload
图片预加载 HTML5学堂:2014年年初的时候,曾经在自己的博客"独行冰海"里写过关于图片预加载和懒加载的博文,当时的文章当中没有写什么关于预载的代码范例,当前打算把预载和懒载分 ...
- 2020-08-23 html的标签预加载 + css的新知识 + JS的json的标准格式 + 软技能的能说会道vs安静做事
2020-08-23 题目来源:http://www.h-camel.com/index.html [html] html的哪个标签可以预加载? 通过浏览器特性来提高资源加载速度的方法: 1.DNS ...
- 超详细的图片预加载和懒加载教程
最近接手一个项目 . 结果光安装依赖都出现了一堆 麻烦 . 好不容易处理完一个 , 又来一个 .头疼啊 看到之前有一些预加载的学习笔记.于是又查查找找 ,想想写写 把预加载和懒加载的笔记写完整 发现制 ...
- jQuery插件之图片预加载
背景: 图片是web页面的重要组成部分,也是前端页面优化的重要内容.当用户访问一个比较庞大的页面时,若相关资源没有提前加载,可能会展示给用户一片空白,从而导致用户流失等:再比如受网速的影响,资源加载时 ...
- vue实现图片预加载实操
业务场景是这样的:在页面里有一个提醒文案,提醒文案里有个按钮,点击按钮会弹出示例弹窗,弹窗里上面有标题,中间一个比较大的图片,27kb:下面有个按钮.在苹果手机使用微信打开时,上面的标题和下面的按钮都 ...
- 【GoLang】《GORM实战》第三篇:关联与预加载
文章目录 关联 分类 重写外键.引用 多态关联 外键约束 关联操作 自动添加关联 关联模式 查询关联 添加关联 替换关联 删除关联 级联删除 清空关联 关联计数 批量处理数据 预加载 Preload ...
最新文章
- JDK 序列化, 碰到serialVersionUID 不一致问题,怎么处理?
- java future用法_Java中的多线程知识点
- @jsonProperty 实现返回自定义属性名字
- BZOJ 3930 [CQOI2015]选数
- [转载] 陈皓:一些重要的算法
- C#汉字转换拼音技术详解(高性能)
- Linux——vim编辑器详解
- linux suse最新版本,SUSE Linux Enterprise Server 15 正式发布
- 线段树i hate it
- android studio 工程rebuild没反应,Andriod Studio Clear Project或Rebuild Project出错
- 大一python题库及答案_Python经典题库及答案-python经典编程题
- XCEL查找SQL SERVER数据库的数据
- [转]2009年河南省高考零分作文:兔子,你就是一个傻B
- 【HNOI2017】大佬-dalao
- 局域网电脑设置固定ip
- Spring的9处调用后置处理器
- 美国J1签证可以免签去哪些国家?
- ubuntu系统vim常用命令学习以及ubuntu软件下载安装
- win10下装win7双系统安装教程
- 用Python爬取大火的《海王》豆瓣评论