WordPress主题Doubt重构-END主题开发前奏

2023
04/07

码农备忘录当前使用的主题是Doubt,是我做的第八款主题,这款主题是在Way主题的基础上修改而来的。Doubt参考了很多国内优秀的极简主题,在主题介绍中将会感谢它们。主题设计时候已是作者进入社会打拼的第七年(2022.2)。当时也处在人生转折期。之所以取名为Doubt,是因为这些年来走了很多弯路,又到了岔路口,我第一次产生了对自己能力以及运气的怀疑。

主题Doubt

在此之前,本站使用的是Bye主题,其与Way是兄弟作品。由于自用,Bye主题并没有开源或出售。

bye主题wordpress

后期之所以弃用Way,是因为2020年底时候,新手站长站域名到期(xszzz.com),由于无心再折腾,决定放弃续费,将其更名为码农备忘录。而码农备忘录定位也只是发表我的一些开发文档或技术文章,所以后期为了突出文章,没有考虑做很多内链等SEO优化的东西,就采用了Way主题,但使用Way主题感觉有些和技术博客格格不入,才对其进行修改,这才有了Doubt主题。

当下我正准备开发END主题,但END主题我尚未进行设计,只是定位为突出文字。在突出文字的基础上,尽量简洁、大气、新颖,尽量多一些交互功能和内链。

我没有做过正常的UI设计,除却工作之外,我自己写东西的时候,一般是在开发中慢慢调整样式,以达到自己想要的效果,但这种粗略的开发模式难以做出优秀的作品。所以此次开发END主题之前我是打算先使用PS大概做一套设计,画一个粗略的图。当然PS我用的还是比较熟练吧,先前我曾使用PS为醉风云资源素材店(淘宝店)进行过全套设计。

但在设计之前,我打算先对本站主题Doubt进行重构,采用先前想好的技术进行实现。这样可以尽快熟悉代码,且在重构过程中,找到一些END主题的设计灵感。

WordPress主题END开发技术选型思路

重构Doubt我会尽量保持原主题的样式不做大的变动,可能只是改变一些小的样式,比如字体样式,文章文本的一些样式。

WordPress主题END开发技术选型思路

2023
03/07

在尝试LitSvelte框架之后,我更倾向于Svelte,其语法更友好。可使用Vite 进行初始化Svelte脚手架。

但测试之后,Svelte并不兼容IE11,但使用@vitejs/plugin-legacy 之后尝试可以兼容(暂时)。

由于此主题页面中,为突出文字内容及不改变原有SEO收录路径,所有有效内容(包括文章列表、文章内容、菜单、部分锚文本)还是采用PHP进行服务端渲染,所以,IE11中应该是会正常体现出这些内容。

当然页面样式可能需要使用CSS Hack或HTML Hack方式进行微调。

//只针对IE
@media screen\0 {
/*样式*/
}
<!--[if IE]> 所有的IE可识别 <![endif]-->

需要说明一下,我并无IE兼容的执念,虽然IE现在已经被微软放弃了,但由于在使用360极速浏览器时候,有一个极速模式和兼容模式,切换兼容模式时就是IE模式。这里有些同学的浏览器默认设置的是兼容模式,所以我是考虑这部分访客。

所以此次开发,我不会太考虑JS脚本的IE兼容性,毕竟现在没多少人用IE了。仅考虑其余低版本浏览器(如Chrome 60)。这里使用@vitejs/plugin-legacy 插件即可兼容,此插件会为低版本浏览器添加语法相对应的polyfill。

打包之后,polyfills文件可能会比较大,但编译结果是按需加载的,所以不用考虑加载效率。况且几百K之内的大小对于现在的网络和浏览器来说,完全不必担心。若想限制polyfills文件大小,可以配置仅需要的语法。

legacy({
      // 版本兼容(如原生ESM在低版本浏览器中不兼容问题)
      targets: [
        "defaults",
        "Chrome >= 52",
        "Edge >= 15",
        "Firefox >= 54",
        "Android >= 40",
        "Safari >= 10.1",
        "iOS >= 10.3",
        "IE >= 11",
      ],
      additionalLegacyPolyfills: ["regenerator-runtime/runtime"], // 面向IE11时需要用的插件
      renderLegacyChunks: true,
      polyfills: [] // 这里配置仅需要转义的语法
 })

由于Svelte 在运行时无虚拟Dom中间层的性能损耗,这里用起来也较Vue更轻量。

需要用到响应式渲染的内容并不太多。这里也仅用到框架的模板引擎功能。

使用框架而不直接用模板引擎的原因是:

  • 一些交互功能使用框架会便于开发,更便于后期主题的功能扩展
  • 便于使用ESM引入一些额外的包
  • 便于使用CSS预处理器(less/sass),这个很关键
  • 便于组件化以及组件复用,这个也很关键

当然,此次开发使用到Svelte可能会有很多坑,比如:

  • 多入口页打包配置
  • 打包结果呈现WordPress主题的php文件结构(这里是否需要手动拆分)

待我慢慢探索。


经过长期开发探索,由于wordpress需考虑到SEO,无法全部使用响应式数据填充页面,而必须使用PHP文件进行开发。再者,Svelte 是编译时框架,种种原因导致无法更好的进行开发环境预览,故放弃Svelte ,还是使用 Vue 。(2024-02-06)

最终方案:

  • 放弃Vite,使用原始开发方式
  • 编译和实时预览使用Node开发预览和编译程序
  • css预处理使用less,less是基于Javascript的,可以在node中使用其render函数进行程序化编译
  • 放弃TS,使用ES-Module方式开发JS
  • 有需要的模块如vue\axios等使用cdn链接引入
  • 放弃更好的兼容性,使用更新的js语法进行开发

WordPress主题筹备,是结束也是开始(END主题)

2023
29/06

醉风云博客十一周年了。

FengYun主题也即将完成它的使命。

深思熟虑之后,我决定,花费很长一段时间,为醉风云博客创作一款新的主题。

她将变得更加简单。她将更加突出文字。

终有一天,大象将重归原野。

原先是考虑使用Vue+Pjax架构打造一款极致主题的(很多博主这样搭配),但考虑到在这里只能用到Vue的模板引擎功能,且使用的频率和Vue的其他功能不会很多,有点大材小用,因为文章和大部分内容要兼顾SEO(PHP服务端渲染)。

若不考虑SEO,当然可以使用Vue+WordPress Rest API 开发单页应用来做一个网站。但是我的博客毕竟这么多年了,之前的文章也都被搜索引擎收录了,我不想改变这些收录的内容及路径。

另外也考虑到Vue的浏览器兼容性(个人博客还是兼容性好点吧),所以若使用Vue,就只能使用Vue2版(兼容IE11)使用CDN方式引入Vue浏览器版本,在Wordpress的主题开发相应的PHP代码中进行嵌入,开发形式不太友好,而且放弃了Webpack或Vite的编译,只能使用线上Babel进行运行时编译,效果不好。当然不编译的话,可以使用一些兼容性好的语法。

方案一

可能尝试使用Pjax+ArtTemplate(Art模板引擎)架构进行开发。Art模板引擎是我用的比较顺手的一款,它的性能很高(先前使用Handlebars/Ejs/Art搭配Koa搭建中间层服务,Art效率及语法更胜一筹),更加轻量(浏览器版本仅 6KB 大小)。

考虑到开发效率,还是尽量用一些新的语法,当然肯定要编译,这里,所有的页面我考虑开发时直接使用HTML静态页进行原始开发(使用MockJS造一些假数据),脚本使用TS,这样便于开发时的预览及编译。等开发完成,再手动将这些静态页其转为PHP(这里可以考虑使用数据化将mock数据和php占位符进行切换实现自动化编译)。

方案二

考虑到打包编译,为了实现热预览、组件化、响应式数据、自动化编译等功能,同时由于HTML原始开发貌似不太友好。为使用Vite打包工具,可能尝试采用Lit 或Svelte框架进行开发。

  • Lit是一个依据 Web-Component 构建的前端结构,同样轻量化(5 KB),简单易学,高效。
  • Svelte 在未使用虚拟DOM的情况下实现了响应式设计,其将更多操作放到编译阶段,解放运行阶段的脚本运行负担,其做法也类似于模板引擎。轻量化(编译后只有几 KB)。

我会尝试这两个框架,选出最优方案进行开发。

最终成型后,打算将其开源。

这款主题,我将其命名为「END」,是结束,也是开始。

届时,等主题完工,本站(码农备忘录)应该也会进行重构,尽情期待。

img元素srcset属性含义及作用

2022
09/10

HTML img元素中有一个很少用的 srcset 属性,用于浏览器根据宽、高和像素密度来加载相应的图片资源。

属性格式:图片地址 宽度描述w 像素密度描述x,多个资源之间用逗号分隔。例如:

<img src="small.jpg " srcset="big.jpg 1440w, middle.jpg 800w, small.jpg 1x" />

上面的例子表示浏览器宽度达到 800px 则加载 middle.jpg ,达到 1400px 则加载 big.jpg。注意:像素密度描述只对固定宽度图片有效。

img 元素的 size 属性给浏览器提供一个预估的图片显示宽度。

属性格式:媒体查询 宽度描述(支持px),多条规则用逗号分隔。

<img src="images/gun.png"
    srcset="images/bg_star.jpg 1200w, images/share.jpg 800w, images/gun.png 320w"
    sizes="(max-width: 320px) 300w, 1200w"/>

上面的例子表示浏览器视口为 320px 时图片宽度为 300px,其他情况为 1200px。

css image-set()

css属性image-set()支持根据用户分辨率适配图像。

body {
    background-image: -webkit-image-set( url(../images/pic-1.jpg) 1x, url(../images/pic-2.jpg) 2x, url(../images/pic-3.jpg) 600dpi);
    background-image: image-set( url(../images/pic-1.jpg) 1x, url(../images/pic-2.jpg) 2x, url(../images/pic-3.jpg) 600dpi);
}

上述代码将会为普通屏幕使用 pic-1.jpg,为高分屏使用 pic-2.jpg,如果更高的分辨率则使用 pic-3.jpg,比如印刷。

Vue/2/3重点总结—理论篇

2022
08/10

一、Vue哪里好?

我认为一个好的框架首先有以下几点必要的特点

  • 简单易学,文档完善,Api清晰易懂,容易上手
  • 不会给程序员的开发过程带来很大的心智负担
  • 社区完善,长期维护,有完整的生态系统
  • 对业务逻辑及数据的关注度优先于框架本身的使用

低学习成本和使用成本

我认为,前端的发展思路永远是:在保证性能的基础上,提升程序员的开发效率为先。

Vue 和其他前端框架相比,在结构、样式、业务分离等方面更清晰彻底,更符合前端多年来的编码习惯,更符合直觉、更容易学习和维护,非常容易与其它库或已有项目整合。

虽然React等视图框架同样有很多优势,也同样简单易学。但其JSX的编码不太符合编码习惯。另外,在操作数据时,React的单向绑定和较为开放的API,封装性较小,容易带来一些心智负担。而vue很多东西都是内置的,写起来方便一些。比如指令,数据监听机制、模板语法封装、计算属性等丰富的API等。

高性能和轻量级

Vue在轻量级的基础上,还兼顾了高性能。相比于其他框架,Vue有更小的编译后体积,以及更快的渲染性能。

HTML模板化的组件结构

如果你喜欢用模板搭建应用,请选择Vue。

对于Web开发者,模板更容易理解。一些资深开发者也更喜欢模板,因为模板可以更好的把布局和功能分割开来。

而React重度依赖JSX,布局和功能代码组合到一起的写法,对于老开发者来说编码习惯不合适。

Vue3.0的加持

Vue3.x版本的上线,速度、性能、体积的优化,尤其是组合式API和函数式的编程思想、对TS更友好的支持是我坚持Vue的动力。

二、Vue打包后有哪些文件?

  • 资源文件:包括图片、字体、样式和直接拷贝的文件
  • app.js:项目中各个模块的逻辑代码,格式被压缩
  • chunk-vendors.js:导入的第三方依赖包。防止该文件体积过大,可以使用webpack的externals配置,可以声明无需打包的依赖。可在使用CDN资源引用。
  • 其他js:使用路由懒加载方式打包的模块逻辑代码
  • xx.js.map:Source map文件,方便我们开发时调试js代码使用
  • index.html:html入口文件,基本没什么内容

三、有没有自己封装过组件?

我用vue开发的所有项目,都是采用组件化的思想开发的。一般我在搭建项目的时候,会创建一个views目录和一个components目录。

  • views目录中放页面级的组件
  • components中放公共组件
  • views中在页面文件夹中也可以添加components文件放置页面内的局部组件或公共组件
  • views或components中可以创建hooks文件夹,用于抽离相同业务逻辑的代码

首先,组件可以提升整个项目的开发效率。能够把页面抽象成多个相对独立的模块,解决了我们传统项目开发:效率低、难维护、复用性低等问题。

组件可以单独引用,也可以使用app.component('TkBadge', TkBadge)函数注册全局组件。

组件的封装要尽量做到低耦合性,方便更好得复用和维护。传参要更加灵活自由。

组件封装要尽量遵循“单一职责原则”,不同组件承担不同职责,互不干扰,可自由组合。这样便于更好的可读性和复用性。

比如封装公共组件如:弹窗、错误提示、自定义右键菜单

UI框架二次封装如:表格二次封装(固定格式的json、分页、查询等)、表单二次封装

前端常用性能优化方案

2022
04/10

一切性能优化是根据网站测试结果去针对性进行的,不需要无脑的进行优化。另外,没有固定的优化策略,不同的项目要分而治之。

性能优化思路

  1. 了解浏览器工作原理及web渲染原理,认识影响性能的因素
  2. 使用如Performance、Lighthouse、PageInsight等工具对性能进行评估,帮助了解短板,说服Linder
  3. 使用控制台面板功能(如请求列表,缓存,资源大小,请求耗时)和DevTools寻找突破口
  4. 使用正确的有针对性的方法解决性能问题

首屏常见性能指标

首屏性能影响因素

  • 域名及DNS解析时间慢
  • 服务器响应时间慢
  • 阻断渲染的js和css
  • 资源加载时间慢
  • 客户端渲染时间慢

减小资源体积

牺牲代码可读性,减小阻塞资源的体积。

  • 对CSS/JS进行压缩(Minifycation),可使用一些成熟的工具如UglifyJS
  • HTML压缩,剔除空格、制表符、去除注释
  • 图片使用JPG/webp/SVG等格式,减少图片大小。在保证质量的前提下可对图片进行压缩
  • 图标可通过font-icon或矢量图(如SVG)来代替
  • 使用Gzip压缩页面(前端提供压缩包,服务器开启压缩,浏览器解压后进⾏再进⾏解析)
  • 移除昂贵的代码库。如果能够手写解决尽量手写。比如使用webpack-bundle-analyzer分析插件可以分析引用库的大小。
  • Tree shaking 消除死码,一些构建工具会帮我们完成。比如Vue3在编译阶段会用到tree shaking按需加载模块和死码消除

控制资源文件加载优先级

浏览器在加载 HTML 内容时,是将 HTML 内容从上至下依次解析,解析到 link 或者 script 标签就会加载 href 或者 src 对应链接内容,为了第一时间展示页面给用户,就需要将 CSS 提前加载,不要受 JS 加载影响。一般情况下都是 CSS 在头部,JS 在底部。

按需加载、懒加载

  • UI框架、自定义组件按需加载
  • 图片或视图区域懒加载:图片懒加载实现
  • 对于响应式页面的图片, 可以使用srcset定义图像, 使浏览器可以在图像之间进行选择,详见

合理利用缓存

资源缓存可以避免过多的TCP连接和响应。数据和状态缓存可以避免过多的计算。

  • HTML离线缓存静态页面,在html标签中添加manifest属性,关联如cache.manifest文件(HTML5支持)
  • 缓存页面资源, 减少浏览器对资源的请求次数
  • 由于HTTP/2多路复用,可以减小缓存粒度,传输轻量、细粒度的资源,以便独立缓存和并行传输。合理安排缓存更新和过期策略,提高缓存的命中率
  • 对请求的静态数据进行缓存(封装),利用浏览器的localStorage和sessionStorage,比如用户信息、菜单

优化资源阻塞,异步加载

优化资源传输效率

减少资源请求数量

虽然HTTP/2的多路复用解决了分域查询的痛点,减少了连接开销,但避免过多的资源数量同样也是有效的优化方案。因为浏览器的并发请求数量是有一定的限制的。比如同一域名谷歌浏览器一是6个。

  • 小图片转为Base64格式的内联代码,避免文件请求
  • 服务端渲染,数据预处理(在服务端已经处理首屏渲染的数据),比如Vue提出的Nuxt.js和React提出的Next.js。
  • 在生产环境关闭source map
  • CSS非必要避免使用import引用

代码层面优化方案

  • HTML减少DOM节点
  • HTML避免空src空href值,会给浏览器增加请求负担
  • CSS优化,尽量使用全局样式,减少行内样式,减少样式选择器的层级
  • CSS属性选择合并
  • 使用简写CSS属性,比如使用margin代替margin-top等
  • JS优化,封装复用方法和工具,封装公共方法
  • JS优化,分解耗时任务:比如使用 web worker 独立运行耗时任务。
  • JS优化,使用更高效的API,比如ES6中的解构赋值,比如Vue3中使用Proxy取代Object.defineProperty
  • 生产环境去除注释。
  • 避免内存泄漏。比如:不必要的全局变量、循环引用、闭包、未清除的定时器、打印到控制台的对象、获取且未使用的Dom对象

减少重排(Reflow)

重排是 DOM 的变化影响到了元素的几何属性(宽和高),浏览器会重新计算元素的几何属性,会使渲染树中受到影响的部分失效,浏览器会验证 DOM 树上的所有其它结点的 visibility 属性,这也是 Reflow 低效的原因。如果 Reflow 的过于频繁,CPU 使用率就会急剧上升。

如何避免重排?

DNS解析优化

减少TCP连接开销

由于TCP连接需要握手,是比较昂贵的操作。所以需要进行优化。

  • 页面的重定向非常昂贵,必须减少页面重定向
  • SPA一定程度上也可以减少页面的重定向
  • 延迟会增加TCP连接开销,使用CDN可以加速

实际上,HTTP/2的多路复用解决了分域查询的痛点,减少了连接开销。

首屏用户体验优化方案

  • 使用骨架屏或Loading图来替换白屏

HTTP2

实际上很多优化方案都是针对HTTP/1版本的,在2015年HTTP/2以后,解决了很多痛点,使得很多优化手段无需再做,甚至用在HTTP/2上会降低性能。了解这些特性很重要,能够避免走入一些性能优化的误区。HTTP/2优势详见

解决痛点的主要特性就是多路复用。HTTP/2对每个服务器只使用一个连接,而不是每个文件一个连接。避免了多次建立连接的开销。如下是HTTP/2无需做的一些优化。

  1. 分域存储。为了实现并行请求文件,你可能把文件分散到了不同的域里,CDN会自动这么做。但分域存储会影响HTTP/2的性能,建议使用HTTP/2友好的分域存储:①让多个域名解析到同一个IP。②确保证书包含通配符,以便所有分域名都可以使用。
  2. 雪碧图。雪碧图把很多图片拼成一个文件,然后通过代码按需取得每个图片。雪碧图在HTTP/2的环境下没太大用处。
  3. 拼接的代码文件。与使用雪碧图的原因类似,很多独立的文件也会被弄成一个,然后浏览器再从其中找到并运行需要的文件。
  4. 插入行内的文件。CSS代码、JavaScript代码,甚至图片等被直接插到HTML文件中的内容。这样可以减少文件传输,代价是初始HTML文件较大。

后端和服务器配合

性能优化并不是前端一个维度可以实现的。服务端需要一定的配合。

  • 合理分配带宽
  • 服务器硬件性能优化
  • 后端要进行并发优化和数据库查询优化

浏览器性能检查工具

Performance(性能)

Performance 是 Chrome 开发者工具中的一个功能,用于记录网页从初始化到运行时的所有性能指标。

使用 Performance 前,我们最好打开 Chrome 的无痕模式。因为 Chrome 上一般有着大量的插件,会或多或少的影响页面的性能,所以我们关掉这个来避免对页面性能的影响。

点击左上角的 Record(小圆点)按钮,Performance 进入 Record 阶段,从此刻开始,它会记录用户的交互以及这些交互对页面性能数据的影响。

生成的 Performance 性能报告,我们先看顶部的三个数据:FPSCPU 以及 NET

  • FPS:主要和动画性能有关,代表每秒帧数。图表中的绿色长条越高,说明FPS越高,用户体验越好。如果其中有红色长条,代表着这部分帧数有卡顿,需要优化
  • CPU:和底部的 Summary 对应,显示了页面加载过程中,各阶段对 CPU 的占用时间,占用时间越多,代表该阶段越需要优化。在 Performance 中,该部分是最需要关注的指标之一。
  • NET:每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间) 深色部分表示传输时间(下载第一个和最后一个字节之间的时间)
  • Main:火焰图。它展现了主线程在 Record 过程中做的所有事情,包括:Loading、Scripting、Rendering、Painting 等等。火焰图的横轴代表着时间,纵轴代表着调用堆栈。每一个长条代表执行了一个事件或函数,长条的长度代表着耗时的长短,如果某个长条右上角是红色的则表示该函数存在性能问题,需要重点关注。
  • DOMContentLoaded :就是 dom 内容加载完毕。 那什么是 dom 内容加载完毕呢?打开一个网页当输入一个 URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发 DOMContentLoaded 事件。而这段时间就是 HTML 文档被加载和解析完成。
  • load: 页面上所有的资源(图片,音频,视频等)被加载以后才会触发 load 事件,简单来说,页面的 load 事件会在 DOMContentLoaded 被触发之后才触发。

Performance 提供的性能监测功能已经较为完备,但是,它有两个问题:

  • 数据缺少实时性
  • 数据面板过于复杂,不够直观

为此,Performance monitor 功能可以实时直观的数据展示页面性能。

Lighthouse面板

Lighthouse 是一个开源的自动化工具,是 Chrome 的一个扩展程序。为 Lighthouse 提供一个您要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告,会对页面的加载进行分析,然后给出提高页面性能的建议。可以对以下分类做报告:

  • 性能
  • 无障碍使用
  • 用户体验
  • SEO 优化
  • 移动设备和桌面设备兼容性

Page Insight 和 Page Speed Insight

这是来自Chrome商店的浏览器插件。可以出具网页性能测试报告。提供性能优化建议等。

前端缓存之Service Worker

2022
04/10

什么是Service Worker

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。Service Worker 可以使你的应用先访问本地缓存资源,包括js、css、png、json等多种静态资源。

Service Worker的特点

  1. 独立于主JavaScript线程(这就意味着它的运行丝毫不会影响我们主进程的加载性能)
  2. 设计完全异步,大量使用Promise(因为通常Service Worker通常会等待响应后继续,Promise再合适不过了)
  3. 不能访问DOM,不能使用XHR和localStorage
  4. Service Worker只能由HTTPS承载(出于安全考虑)

Service Worker用法

注册

使用 ServiceWorkerContainer.register() 方法注册service worker。

// sw.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/loading-page-sw.js', {scope: '/'}).then(function(reg) {
	// reg可以查看当前sw的状态和作用域等 
    }).catch(function(error) {
        // registration failed
        console.log('Registration failed with ' + error);
    });
}

缓存处理

  • caches.open(SW_VERSION):打开缓存, SW_VERSION: 版本
  • cache.addAll(CACHE_FILE_LIST):添加缓存, CACHE_FILE_LIST:文件路径列表
  • caches.keys():获取本地存储的版本集合
  • caches.delete(key): 删除某个版本的缓存信息
  • cache.put(event.request, responseClone):手动添加缓存,参数为request,response

下面附完整代码

// loading-page-sw.js
const SW_VERSION = 'V1';
const CACHE_FILE_TYPE = [ 'js','css', 'html','jpg','json','png''mp3','wav','mp4','ttf'];
//需要确认缓存的文件
const CACHE_FILE_LIST = [];
// 需要忽悠的文件列表
const IGNORE_FILE_LIST = [
  '/test/index.js',
];
/**
 * 是否是对应的文件类型
 * @param {*} url 
 */
function isAcceptFile(url) {
  var r = new RegExp("\\.(" + CACHE_FILE_TYPE.join('|') + ")$");
  return r.test(url);
}
/**
 * 检查文件名
 */
function checkIgnoreFileName(url) {
  var r = new RegExp("(" + IGNORE_FILE_LIST.join('|') + ")$");
  return r.test(url);
}
self.addEventListener('install', function(event) {
    event.waitUntil(
      caches.open(SW_VERSION).then(function(cache) {
        return cache.addAll(CACHE_FILE_LIST);
      })
    );
  });
self.addEventListener('activate', function(event) {
    var cacheWhitelist = [SW_VERSION];
    event.waitUntil(
      caches.keys().then(function(keyList) {
        return Promise.all(keyList.map(function(key) {
          if (cacheWhitelist.indexOf(key) === -1) {
            return caches.delete(key);
          }
        }));
      })
    );
  });
// 监听浏览器的所有fetch请求,对已缓存的资源使用本地缓存回复  
self.addEventListener('fetch', function(event) {
    const {method, url} = event.request;
    event.respondWith(
      caches.match(event.request).then(function(response) {
          if (response !== undefined) {
              return response;
          } else {
              return fetch(event.request).then(function (response) {
                  let responseClone = response.clone();
                  if (method === 'POST') {
                    return response
                  }
                  if (!isAcceptFile(url)) {
                    return response
                  }
                  if (checkIgnoreFileName(url)) {
                    return response
                  }
                  caches.open(SW_VERSION).then(function (cache) {
                    cache.put(event.request, responseClone);
                  });
                  return response;
              }).catch(function (error) {
                 return Promise.reject(error);
              });
          }
      })
    );
  });

service worker调试检查

查看service worker进程状态

service worker实际上提供的是本地缓存服务,所以和我们平时查看localStorage差不多,打开谷歌浏览器调试中心,在Application栏下,就能看到Service Woerkers,

可通过右侧Unregister来注销service worker进程。

查看本地存储信息

service worker会根据我们的版本在本地存储相应版本的文件,如图:

V1: 是我们自定义的版本号

缓存的文件已列表的形式被列出,信息有名称、返回类型、上下文长度、缓存时间等。

HTTP缓存

当我们的页面发起资源请求时,浏览器会通过缓存等手段来尽快响应,避免不必要的http消耗,所以我们经常见到:Memory Cache、Disk Cache、Push Cache,现在又多了一种ServiceWorker。我们来简单对比如下:

ServiceWorker

当我们启用ServiceWorker时,浏览器就会显示我们的资源来自ServiceWorker。Service Worker的缓存有别与其他缓存机制,我们可以自由控制缓存、文件匹配、读取缓存,并且是持续性的。当ServiceWorker没有命中时才会去重新请求数据。

Memory Cache

Memory Cache为内存中的缓存,读取内存中的资源是非常快的。 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束。内存读取虽然快且高效,但它是短暂的,当浏览器或者tab页面关闭,内存就会被释放了。而且内存占用过多会导致浏览器占用计算机过大内存。

Disk Cache

Disk Cache 将资源存储在硬盘中,读取速度次于Memory Cache。

优点:可长期存储,可定义时效时间、容量大;缺点:读取速度慢。

根据http header 请求头触发的缓存策略来做缓存,包含强缓存和协商缓存

Push Cache

当以上三种缓存都没有命中时才会使用Push Cache。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。这种缓存一般用不到。

我们通过控制台network,最直观的查看资源用的缓存方式。如图所示:

Service Worker注销和删除

我们对Service Worker已经做了用法说明,并和其他缓存做了对比;最后我们来说说如何注销Service Worker和删除本地缓存吧!

注销

通过调用unregister()函数来注销,代码如下:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.getRegistrations()
    .then(function(registrations){
        registrations.forEach(function(registration) {
            registration.unregister();
        })
    })
}

删除

通过调用caches.delete()删除对,代码如下:

if (window.caches && caches.keys) {
    caches.keys().then(function(keys) {
        keys.forEach(function(key) {
            caches.delete(key)
        })
    })
}

写在最后

由于Service Worker是单独运行环境,独立于主JavaScript进程的,导致前端获取的navigator.userAgent和后台获取的userAgent不一样,若在项目中需要使用Service worker时需观察自身业务是否有影响,多做测试观察。

优化资源阻塞Preload、Prefetch和Preconnect

2022
04/10

本文介绍了前端开发如何优化资源的加载,减少资源往返时间。

Preload

Preload 是一个新的控制特定资源如何被加载的新的 Web 标准,这是已经在 2016 年 1 月废弃的 subresource prefetch 的升级版。这个指令可以在 <link> 中使用,比如 <link rel="preload">。一般来说,最好使用 preload 来加载你最重要的资源,比如图像,CSS,JavaScript 和字体文件。这不要与浏览器预加载混淆,浏览器预加载只预先加载在HTML中声明的资源。preload 指令事实上克服了这个限制并且允许预加载在 CSS 和JavaScript 中定义的资源,并允许决定何时应用每个资源。

Preload 与 prefetch 不同的地方就是它专注于当前的页面,并以高优先级加载资源,Prefetch 专注于下一个页面将要加载的资源并以低优先级加载。同时也要注意 preload 并不会阻塞 window 的 onload 事件。(preload 是不阻塞页面渲染的!)

使用 Preload 的好处

使用 preload 指令的好处包括:

  • 允许浏览器来设定资源加载的优先级因此可以允许前端开发者来优化指定资源的加载。
  • 赋予浏览器决定资源类型的能力,因此它能分辨这个资源在以后是否可以重复利用。
  • 浏览器可以通过指定 as 属性来决定这个请求是否符合 content security policy。
  • 浏览器可以基于资源的类型(比如 image/webp)来发送适当的 accept 头。
  • 预加载是异步非阻塞

举例

这里有一个非常基本的预加载图像的例子:

<link rel="preload" href="image.png">

这里有一个预加载字体的例子,记住:如果你的预加载需要 CORS 的跨域请求,那么也要加上 crossorigin 的属性。

<link rel="preload" href="https://example.com/fonts/font.woff" as="font" crossorigin>

这里有一个通过 HTML 和 JavaScript 预加载样式表的例子:

<link rel="preload" href="/css/mystyles.css" as="style">

<script>
var res = document.createElement("link");
res.rel = "preload";
res.as = "style";
res.href = "css/mystyles.css";
document.head.appendChild(res);
</script>

Prefetch

Prefetch 是一个低优先级的资源提示,允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中。一旦一个页面加载完毕就会开始下载其他的资源,然后当用户点击了一个带有 prefetched 的连接,它将可以立刻从缓存中加载内容。有三种不同的 prefetch 的类型,link,DNS 和 prerendering,下面来详细分析。

像上面提到的,link prefetching 假设用户将请求它们,所以允许浏览器获取资源并将他们存储在缓存中。浏览器会寻找 HTML <link> 元素中的 prefetch 或者 HTTP 头中如下的 Link:

  • HTML:<link rel="prefetch" href="/uploads/images/pic.png">
  • HTTP Header:Link: </uploads/images/pic.png>; rel=prefetch

“这项技术有为很多有交互网站提速的潜力,但并不会应用在所有地方。对于某些站点来说,太难猜测用户下一步的动向,对于另一些站点,提前获取资源可能导致数据过期失效。还有很重要的一点,不要过早进行 prefetch,否则会降低你当前浏览的页面的加载速度 —— Google Developers”

除了 Safari, iOS Safari 和 Opera Mini,现代浏览器已经支持了 link Prefetch,Chrome 和 Firefox 还会在网络面板上显示这些 prefetched 资源。

DNS Prefetching

DNS prefetching 允许浏览器在用户浏览页面时在后台运行 DNS 的解析。如此一来,DNS 的解析在用户点击一个链接时已经完成,所以可以减少延迟。可以在一个 link 标签的属性中添加 rel="dns-prefetch' 来对指定的 URL 进行 DNS prefetching,我们建议对 Google fonts,Google Analytics 和 CDN 进行处理。

“DNS 请求在带宽方面流量非常小,可是延迟会很高,尤其是在移动设备上。通过 prefetching 指定的 DNS 可以在特定的场景显著的减小延迟,比如用户点击链接的时候。有些时候,甚至可以减小一秒钟的延迟 —— Mozilla Developer Network”

这也对需要重定向的资源很有用,如下:

<link rel="dns-prefetch" href="//fonts.googleapis.com"> 
<link rel="dns-prefetch" href="//www.google-analytics.com"> 
<link rel="dns-prefetch" href="//opensource.keycdn.com"> 
<link rel="dns-prefetch" href="//cdn.domain.com">

不过要注意的是 Chrome 已经在敲击地址栏的时候做了类似的事情,比如 DNS preresolve 和 TCP preconnect,这些措施太酷了!你可以通过 chrome://dns/ 来查看你的优化列表。

你可以利用 Pagespeed 的过滤器 insert_dns_prefetch 来自动化的为所有域名插入 <link rel="dns-prefetch">

DNS prefetch 已经被除了 Opera Mini 之外的所有现代浏览器支持了。

Prerendering

Prerendering 和 prefetching 非常相似,它们都优化了可能导航到的下一页上的资源的加载,区别是 prerendering 在后台渲染了整个页面,整个页面所有的资源。如下:

<link rel="prerender" href="https://www.keycdn.com">

prerender 提示可以用来指示将要导航到的下一个 HTML:用户代理将作为一个 HTML 的响应来获取和处理资源,要使用适当的 content-types 获取其他内容类型,或者不需要 HTML 预处理,可以使用 prefetch。—— W3C”

要小心的使用 prerender,因为它将会加载很多资源并且可能造成带宽的浪费,尤其是在移动设备上。还要注意的是,你无法在 Chrome DevTools 中进行测试,而是在 chrome://net-internals/#prerender 中看是否有页面被 prerendered 了,你也可以在 prerender-test.appspot.com 进行测试。

除了 Mozilla Firefox,Safari,iOS Safari,Opera Mini 和 Android 浏览器外的一些现代浏览器已经支持了 prerendering。

除了多余的资源加载外,使用 prefetch 还有一切 额外的副作用,比如对隐私的损害:

  • Web 统计将会收到影响而变大,尽管 Google 说已经限制了这个标签。看看这个关于页面分析将会被影响而在一次点击时产生两个 session 的 文章。
  • 由于可能从未访问的站点下载了更多的页面(尤其是隐匿下载正在变得更加先进和多样化),用户的安全将面临更多的风险。
  • 如果预取访问未经授权的内容,用户可能违反其网络或组织的可接受使用策略。

可以读一下我们对 prefetching 的一篇深入分析的文章。

Preconnect

preconnect 允许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析,TLS 协商,TCP 握手,这消除了往返延迟并为用户节省了时间。

Preconnect 是优化的重要手段,它可以减少很多请求中的往返路径,在某些情况下可以减少数百或者数千毫秒的延迟。

preconnect 可以直接添加到 HTML 中 link 标签的属性中,也可以写在 HTTP 头中或者通过 JavaScript 生成,如下是一个为 CDN 使用 preconnect 的例子:

<link href="https://cdn.domain.com" rel="preconnect" crossorigin>

如下是为 Google Fonts 使用 preconnect 的例子,通过给 fonts.gstatic.com 加入 preconnect 提示,浏览器将立刻发起请求,和 CSS 请求并行执行。在这个场景下,preconnect 从关键路径中消除了三个 RTTs(Round-Trip Time) 并减少了超过半秒的延迟,lya Grigorik 的 eliminating RTTS with preconnect 一文中有更详细的分析。

使用 preconnect 是个有效而且克制的资源优化方法,它不仅可以优化页面并且可以防止资源利用的浪费。

前端常见性能指标

2022
04/10

文档流加载生命周期

DOMContentLoaded

是指页面元素加载完毕,但是一些资源比如图片还无法看到,但是这个时候页面是可以正常交互的,比如滚动,输入字符等。 jQuery 中经常使用的 $(document).ready() 其实监听的就是 DOMContentLoaded 事件。

load

是指页面上所有的资源(图片,音频,视频等)加载完成。jQuery 中 $(document).load() 监听的是 load 事件。

<span class="hljs-comment">// load</span>
<span class="hljs-variable language_">window</span>.<span class="hljs-property">onload</span> = <span class="hljs-keyword">function</span>() {};

<span class="hljs-comment">// DOMContentLoaded</span>
<span class="hljs-keyword">function</span> <span class="hljs-title function_">ready</span>(<span class="hljs-params">fn</span>) {
    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">addEventListener</span>) {
        <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">addEventListener</span>(
            <span class="hljs-string">'DOMContentLoaded'</span>,
            <span class="hljs-keyword">function</span>() {
                <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">removeEventListener</span>(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-variable language_">arguments</span>.<span class="hljs-property">callee</span>, <span class="hljs-literal">false</span>);
                <span class="hljs-title function_">fn</span>();
            },
            <span class="hljs-literal">false</span>
        );
    }
    <span class="hljs-comment">// 如果 IE</span>
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">attachEvent</span>) {
        <span class="hljs-comment">// 确保当页面是在iframe中加载时,事件依旧会被安全触发</span>
        <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">attachEvent</span>(<span class="hljs-string">'onreadystatechange'</span>, <span class="hljs-keyword">function</span>() {
            <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">readyState</span> == <span class="hljs-string">'complete'</span>) {
                <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">detachEvent</span>(<span class="hljs-string">'onreadystatechange'</span>, <span class="hljs-variable language_">arguments</span>.<span class="hljs-property">callee</span>);
                <span class="hljs-title function_">fn</span>();
            }
        });
        <span class="hljs-comment">// 如果是 IE 且页面不在 iframe 中时,轮询调用 doScroll 方法检测DOM是否加载完毕</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">documentElement</span>.<span class="hljs-property">doScroll</span> && <span class="hljs-keyword">typeof</span> <span class="hljs-variable language_">window</span>.<span class="hljs-property">frameElement</span> === <span class="hljs-string">'undefined'</span>) {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-variable language_">document</span>.<span class="hljs-property">documentElement</span>.<span class="hljs-title function_">doScroll</span>(<span class="hljs-string">'left'</span>);
            } <span class="hljs-keyword">catch</span> (error) {
                <span class="hljs-keyword">return</span> <span class="hljs-built_in">setTimeout</span>(<span class="hljs-variable language_">arguments</span>.<span class="hljs-property">callee</span>, <span class="hljs-number">20</span>);
            }
            <span class="hljs-title function_">fn</span>();
        }
    }
}

readystatechange

documentreadyState属性来描述documentloading状态, readyState的改变会触发readystatechange事件.

  • loading: 文档文在加载
  • interactive: 文档结束加载并被解析, 但是图片, 样式, frame之类的子资源仍在加载
  • complete: 文档和子资源已经结束加载, 该状态表明将要触发loading事件.

因此, 我们同样可以使用该事件来判断dom的加载状态.

beforeunload

在浏览器窗口, 文档或器资源将要卸载时, 会触发beforeunload事件, 这个文档依然是可见的, 并且这个事件在这一刻是可以取消的.

unload

当文档或者一个资资源将要被卸载时, 在beforeunload,pagehide时间之后触发, 文档会处于一个特定状态:

  • 所有资源仍存在
  • 对于终端用户所有资源均不可见
  • 界面交互无效
  • 错误不会停止卸载文档的过程.
document.addEventListener("DOMContentLoaded", function (event) {
    console.log("初始DOM 加载并解析");
});
window.addEventListener("load", function (event) {
    console.log("window 所有资源加载完成");
});
document.onreadystatechange = function () {
    console.log(document.readyState)
    if (document.readyState === "complete") {
        console.log('初始DOM,加载解析完成')
    }
}
window.addEventListener("beforeunload", function (event) {
    console.log('即将关闭')
    event.returnValue = "\o/";
});
window.addEventListener('unload', function (event) {
    console.log('即将关闭1');
});

基本指标介绍及优化方案

首次绘制(FP,白屏)

FP (First Paint)是时间线上的第一个“时间点”,是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间,简而言之就是浏览器第一次发生变化的时间。

白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。首次绘制 FP 包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻。

当浏览器开始渲染页面,白屏触发,这时候你如果设置了背景颜色的话,就可以看到页面出现了背景色。

白屏会在页面加载之前触发,在这段时间里,不会呈现任何内容和信息给用户。虽然背景色会很快完成绘制,但是实际的内容和交互可能要花很长的时间去加载,因此,白屏时间过长,会让用户认为我们的页面不能用或可用性差。

白屏示例图

影响白屏时间的因素: 网络、服务端性能、前端页面结构设计

计算:白屏时间 = 地址栏输入网址后回车 – 浏览器出现第一个元素

通常认为浏览器开始渲染<body>标签或者解析完<head>的时间是白屏结束的时间点。

<head>
...
<script>
    // 通常在head标签尾部时,打个标记,这个通常会视为白屏时间
    performance.mark("first paint time");
</script>
</head>
<body>
...
<script>
    // get the first paint time
    const fp = Math.ceil(performance.getEntriesByName('first paint time')[0].startTime);
</script>
</body>

一种比较简单的做法是在 body 标签之前获取当前时间 – performance.timing.navigationStart

<head>
<script>
  const fp = Date.now() - performance.timing.navigationStart
</script>
</head>

也可以使用其他的计算方法:白屏时间 = 页面开始展示的时间点 – 开始请求的时间点。

优化:可以使用骨架屏或Loading图来替换白屏,在白屏结束时移除相关DOM节点。

首次内容绘制(FCP,首屏)

FCP(First Contentful Paint),是指浏览器从响应用户输入网络地址,在页面首次绘制文本,图片(包括背景图)、非白色的 canvas 或者 SVG 才算做 FCP。
首屏渲染完成时间没有确切的标准。具体是哪个DOM元素绘制完成,或者是整个DOM绘制完成,由开发者定义。 实践中, 可以将页面评分最高的可见内容出现在屏幕上的时间作为FCP时间。

首屏决定了网页的用户体验,因为它会标记实际内容何时加载到页面中,而不仅仅是标记页面的变化状态。因为关注的是内容,所以该指标可以了解用户何时收到消耗性信息,比如文本,视觉效果等,这比通过背景改变或样式改变对用户体验进行评估更有用。

首屏示例图

计算:首屏时间 = 地址输入网址后回车 – 浏览器第一屏渲染完成

关于首屏时间是否包含图片加载,通常有不同的说法。若认为不包含图片加载,则可在次元素后面加入 script 计算首屏完成的时间。(由于浏览器解析 HTML 是按照顺序解析的, 当解析到某个元素的时候, 认为首屏完成了),比如认为body渲染完成为首屏时间,则可如下计算:

<body> 
... 
<script> 
// 首屏时间
const fcp = Date.now() - performance.timing.navigationStart
</script>
</body>

如果认为首屏包含图片加载,可以统计首屏加载最慢的图片是否加载完成,加载完了,记录结束时间。由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。我们只需要监听首屏内所有的图片的 onload 事件,获取图片 onload 时间最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。

<body>
<div class="app-container">
    <img src="a.png" onload="imageLoaded()">
    <img src="b.png" onload="imageLoaded()">
    <img src="c.png" onload="imageLoaded()">
</div>
<script>
    // 根据首屏中的核心元素确定首屏时间
    performance.clearMarks("imageLoaded");
    performance.mark("imageLoaded");
    function imageLoaded() {
        performance.clearMarks("imageLoaded");
        performance.mark("imageLoaded");
    }
</script>
...
...
<script>
    // get the first screen loaded time
    const fmp = Math.ceil(performance.getEntriesByName('imageLoaded')[0].startTime);
</script>
</body>

可交互时间(TTI)

TTI(Time to Interactive),翻译为“可交互时间”表示网页第一次完全达到可交互状态的时间点。可交互状态指的是页面上的 UI 组件是可以交互的(可以响应按钮的点击或在文本框输入文字等),不仅如此,此时主线程已经达到“流畅”的程度,主线程的任务均不超过 50 毫秒。在一般的管理系统中,TTI 是一个很重要的指标。

关于 TTI 可以首先了解下谷歌提出的性能模型 RAIL:

  1. 响应:输入延迟时间(从点按到绘制)小于 100 毫秒。用户点按按钮(例如打开导航)。
  1. 动画:每个帧的工作(从 JS 到绘制)完成时间小于 16 毫秒。用户滚动页面,拖动手指(例如,打开菜单)或看到动画。拖动时,应用的响应与手指位置有关(例如,拉动刷新、滑动轮播)。此指标仅适用于拖动的持续阶段,不适用于开始阶段。
  1. 空闲:主线程 JS 工作分成不大于 50 毫秒的块。用户没有与页面交互,但主线程应足够用于处理下一个用户输入。
  1. 加载:页面可以在 1000 毫秒内就绪。用户加载页面并看到关键路径内容。

我们可以通过 domContentLoadedEventEnd 来粗略的进行估算:

TTI:domContentLoadedEventEnd - navigationStart

谷歌实验室也提供了更加便捷准确的 api 包进行测算 tti-polyfil:

import ttiPolyfill from "./path/to/tti-polyfill.js";
ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
  // Use `tti` value in some way.
});

最大内容绘制(LCP)

LCP(Largest Contentful Paint,最大内容元素渲染)表示可视区“内容”最大的(绘制面积最大)可见元素开始出现在屏幕上的时间点。为了提供良好的用户体验, 网站应该努力在开始加载页面的前2.5s内进行最大内容渲染。

LCP不会计算所有的元素, 它只关注:

  • img 元素
  • images中的svg元素
  • video元素
  • 通过url()函数加载背景图片的元素
  • 包含文本节点或者其他内联文本元素子级的块级元素

影响LCP较差的最常见原因是:

  • 服务器响应时间慢
  • 阻断渲染的js和css
  • 资源加载时间慢
  • 客户端渲染

改善LCP具体的措施有:

在过去,我们也有推荐的性能指标,如:FMP (First Meaningful Paint)SI (Speed Index)可以帮我们捕获更多的首次渲染之后的加载性能,但这些过于复杂,而且很难解释,也经常出错,没办法确定主要内容什么时候加载完。

根据W3C Web 性能工作组的讨论和 Google 的研究,发现度量页面主要内容的可见时间有一种更精准且简单的方法是查看 “绘制面积” 最大的元素何时开始渲染。

所谓绘制面积可以理解为每个元素在屏幕上的 “占地面积” ,如果元素延伸到屏幕外,或者元素被裁切了一部分,被裁切的部分不算入在内,只有真正显示在屏幕里的才算数。图片元素的面积计算方式稍微有点不同,因为可以通过 CSS 将图片扩大或缩小显示,也就是说,图片有两个面积:“渲染面积”与“真实面积”。在 LCP 的计算中,图片的绘制面积将获取较小的数值。例如:当“渲染面积”小于“真实面积”时,“绘制面积”为“渲染面积”,反之亦然。

页面在加载过程中,是线性的,元素是一个一个渲染到屏幕上的,而不是一瞬间全渲染到屏幕上,所以“渲染面积”最大的元素随时在发生变化。如果使用 PerformanceObserver 去捕获 LCP,会发现每当出现“渲染面积”更大的元素,就会捕获出一条新的性能条目。

如果元素被删除,LCP 算法将不再考虑该元素,如果被删除的元素刚好是 “绘制面积” 最大的元素,则使用新的 “绘制面积” 最大的元素创建一个新的性能条目。

该过程将持续到用户第一次滚动页面或第一次用户输入(鼠标点击,键盘按键等),也就是说,一旦用户与页面开始产生交互,则停止报告新的性能条目。

可以直接使用 PerformanceObserver 来捕获 LCP:

const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  const lcp = lastEntry.renderTime || lastEntry.loadTime;
  console.log("LCP:", lcp);
});
observer.observe({ entryTypes: ["largest-contentful-paint"] });

LCP 也不是完美的,也很容易出错,它会在用户进行交互后就停止捕获,可能会获取到错误的结果,如果有占据页面很大的轮播图也会产生问题会不断的更新 LCP

LCP 也有现成的计算工具库 web-vitals:

import { getLCP } from "web-vitals";

// Measure and log the current LCP value,
// any time it's ready to be reported.
getLCP(console.log);

首次可交互时间(FID)

FID(First Input Delay)即记录用户和页面进行首次交互操作所花费的时间,FID指标影响用户对页面交互性和响应性的第一影响。为了提供良好的用户体验,站点应该使首次输入延迟小于100毫秒。

FID发生在FCP和TTI之间,应为这个阶段虽然页面已经显示出部分的内容, 但尚不具备完全的可交互性. 这个阶段的用户交互往往有比较大的延迟。

浏览器接收到用户输入操作的时候,主线程正在忙于执行一个耗时比较长的任务,只有当这个任务执行完成以后, 浏览器才能响应用户的输入操作,他必须等待的时间就是此页面上该用户的FID值。

优化:

一方面,减少js的执行时间:

  • 缩小、压缩js文件
  • 延迟加载首屏不需要的js
  • 减少未使用的polyfill

另一方面,我们可以分解耗时任务:比如使用 web worker 独立运行耗时任务。

视觉稳定性(CLS)

布局偏移分值(Cumulative Layout Shift)是衡量页面整个生命周期中样式意外移动的指标。

页面内容位置意外变动是导致视觉不稳定的因素。其原因由于异步加载的资源或DOM元素动态添加而导致的。

CLS是通过测量页面的整个生命周期中发生的每个意外的样式移动的所有单独布局更改得分的总合。为了提供良好的用户体验, 网站应该努力让cls分数小于0.1。

计算:布局偏移分值 = 影响分数 * 距离分数

  • 影响分数:前一帧和当前帧所有不稳定元素的可见区域的并集(占视口总面积的部分)
  • 距离分数:是任何不稳定元素在框架中移动的最大距离/视口的最大尺寸

优化:

1. 不要使用无尺寸元素

图片和视频元素需要始终包含widthheight尺寸属性, 现代浏览器会根据widthheight设置图片的默认宽高比. 或者直接使用aspect-radio也可以提前指定宽高比:

img {
    aspect-ratio: attr(width) / attr(height);
}

对于响应式图片, 可以使用srcset定义图像, 使浏览器可以在图像之间进行选择, 以及每个图像的大小:

<img 
    width="1000" 
    height="1000"
    src="puppy-1000.jpg"
    srcset="puppy-1000.jpg 1000w,
            puppy-2000.jpg 2000w,
            puppy-3000.jpg 3000w"
    alt="ConardLi"
/>

2. 其他

  • 永远不要在现有内容之上插入内容, 除非是响应用户交互. 这能确保预期的布局变化
  • 宁可转换动画, 也不要转换触发布局变化的属性的动画.
  • 提前给广告位预留空间
  • 警惕字体变化, 使用font-display告诉浏览器默认使用系统字体进行渲染, 当自定义字体下载完成之后在进行替换
@font-face {
  font-family: 'Pacifico';
  font-style: normal;
  font-weight: 400;
  src: local('Pacifico Regular'), local('Pacifico-Regular'), url(https://fonts.gstatic.com/xxx.woff2) format('woff2');
  font-display: swap;
}

此外可以使用<link rel='preload'>提前加载字体文件。

指标获取

可以使用Chrome插件web-vitals-extension来获取一些指标。

Google提供了web-vitals来让我们便捷的获取CLS, FID, LCP这三个指标。

import {getCLS, getFID, getLCP} from 'web-vitals';

getCLS(console.log, true);
getFID(console.log); // Does not take a `reportAllChanges` param.
getLCP(console.log, true);

performance 介绍

performance 对象是专门用来用于性能监控的对象,内置了一些前端需要的性能参数。

performance.now()方法

performance.now() 返回 performance.navigationStart 至当前的毫秒数。performance.navigationStart 是下文将介绍到的可以说是浏览器访问最初的时间测量点。

performance.now(); // 24614164.599999994

performance.timing

  1. navigationStart: 表示从上一个文档卸载结束时的 unix 时间戳,如果没有上一个文档,这个值将和 fetchStart 相等。
  2. unloadEventStart: 表示前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0。
  3. unloadEventEnd: 返回前一个页面 unload 时间绑定的回掉函数执行完毕的时间戳。
  4. redirectStart: 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0。
  5. redirectEnd: 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0。
  6. fetchStart: 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前。
  7. domainLookupStart/domainLookupEnd: DNS 域名查询开始/结束的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
  8. connectStart: HTTP(TCP)开始/重新 建立连接的时间,如果是持久连接,则与 fetchStart 值相等。
  9. connectEnd: HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等。
  10. secureConnectionStart: HTTPS 连接开始的时间,如果不是安全连接,则值为 0。
  11. requestStart: HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。
  12. responseStart: HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存。
  13. responseEnd: HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存。
  14. domLoading: 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件。
  15. domInteractive: 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件,注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源。
  16. domContentLoadedEventStart: DOM 解析完成后,网页内资源加载开始的时间,在 DOMContentLoaded 事件抛出前发生。
  17. domContentLoadedEventEnd: DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)。
  18. domComplete: DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。
  19. loadEventStart: load 事件发送给文档,也即 load 回调函数开始执行的时间。
  20. loadEventEnd: load 事件的回调函数执行完毕的时间。
// 计算加载时间
function getPerformanceTiming() {
    var t = performance.timing;
    var times = {};
    // 页面加载完成的时间,用户等待页面可用的时间
    times.loadPage = t.loadEventEnd - t.navigationStart;
    // 计算dom渲染耗时
    times.domTimes = timing.domComplete - timing.domLoading
    // 解析 DOM 树结构的时间
    times.domReady = t.domComplete - t.responseEnd;
    // 重定向的时间
    times.redirect = t.redirectEnd - t.redirectStart;
    // DNS 查询时间
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
    // 读取页面第一个字节的时间
    times.ttfb = t.responseStart - t.navigationStart;
    // 资源请求加载完成的时间
    times.request = t.responseEnd - t.requestStart;
    // 执行 onload 回调函数的时间
    times.loadEvent = t.loadEventEnd - t.loadEventStart;
    // DNS 缓存时间
    times.appcache = t.domainLookupStart - t.fetchStart;
    // 卸载页面的时间
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;
    return times;
}

performance.navigation

redirectCount: 0 // 页面经过了多少次重定向

type: 0

  • 0 表示正常进入页面;
  • 1 表示通过 window.location.reload() 刷新页面;
  • 2 表示通过浏览器前进后退进入页面;
  • 255 表示其它方式

performance.memory

  • jsHeapSizeLimit: 内存大小限制
  • totalJSHeapSize: 可使用的内存
  • usedJSHeapSize: JS 对象占用的内存

performance.getEntries()方法

浏览器获取网页时,会对网页中每一个对象(脚本文件、样式表、图片文件等等)发出一个 HTTP/HTTPS 请求。performance.getEntries() 方法以数组形式,返回一个 PerformanceEntry 列表,这些请求的时间统计信息,有多少个请求,返回数组就会有多少个成员。

关于前端监控方案

2022
27/09

当有人问起:你们的公司的这款应用用户体验怎么样呀?访问量怎么样?此时,你该怎么回答呢?你会回答:UV(独立访客数,以cookie或token为依据)、PV(访问量,页面浏览次数) ,IP(独立IP数)等页面流量指标。

秒开率、RTT(延迟,数据往返时间)、TTI(页面可交互时间,用户与页面进行互动之前所花费的时间) 、FCP(首屏时间)、 FP(白屏时间)等性能指标。详见:前端页面的常见性能指标

那么,这些数据是哪里来的呢?显而易见,这些数据都来自前端监控系统。

前端监控的意义

  1. 性能监控,避免由性能问题影响用户的留存率
  2. 错误监控,避免用户流失
  3. 数据上报,根据用户行为和数据进行分析

错误采集方式

错误监控可以分为:脚本错误监控请求错误监控资源错误监控

脚本错误监控

编译时错误

一般在开发阶段就会发现,配合 lint 工具比如 eslint、tslint 等以及 git 提交插件比如 husky 等,基本可以保证线上代码不出现低级的编译时错误。大厂一般都有发布前置检测平台,能够在发布前提前发现编译时错误。

运行时错误

搭建前端检测平台。

前端检测平台错误捕获的机制有:

  • Js 通过 try catch 捕获错误。但是,try catch 捕获错误是侵入式的,需要在开发代码时即提前进行处理,无法做到在所有可能产生错误的代码片段中都嵌入 try catch
  • 全局捕获脚本错误:根据 window.onerror 事件进行全局捕获,但只能绑定一个回调函数,且回调函数的参数过于离散,使用不方便,且不能监听到资源错误。可以使用addEventListener,但只有 window.onerror 才能阻止抛出错误到控制台
  • 以上两种方法无法捕获 Promise 错误,需要用到 Promise 错误事件:unhandledrejection 以及 rejectionhandled

try-catch缺点:

  1. 需要提前进行处理,无法全局捕获
  2. 通过 try catch 包裹,影响代码可读性
  3. 无法处理语法错误,比如使用了中文标点
  4. 无法处理异步中的错误,比如 setTimeout
/**
 * @description window.onerror 全局捕获错误
 * @param event 错误信息,如果是
 * @param source 错误源文件URL
 * @param lineno 行号
 * @param colno 列号
 * @param error Error对象
 */
window.onerror = function (event, source, lineno, colno, error) {
  // 上报错误
  // 如果不想在控制台抛出错误,只需返回 true 即可
};

//-----------------------------------

/**
 * @param event 事件名
 * @param function 回调函数
 * @param useCapture 回调函数是否在捕获阶段执行,默认是false,在冒泡阶段执行
 */
window.addEventListener('error', (event) => {
  // addEventListener 回调函数的离散参数全部聚合在 error 对象中
  // 上报错误
}, true)

// Promise错误捕获------------------------
// 当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件。
// 当 Promise 被 reject 且有 reject 处理器的时候,会触发 rejectionhandled 事件。

// unhandledrejection
window.addEventListener('unhandledrejection', (event) => {
  console.log(event)
}, true);

// unhandledrejection 备选处理方案
window.onunhandledrejection = function (error) {
  console.log(error)
}

// rejectionhandled 推荐处理方案
window.addEventListener('rejectionhandled', (event) => {
  console.log(event)
}, true);

// rejectionhandled 备选处理方案
window.onrejectionhandled = function (error) {
  console.log(error)
}

框架错误

框架提供了 API 来捕获全局错误。在 Vue 中,提供了 errorHandler  来捕获错误(vue2vue3)。

微信小程序提供了app.onError的方法。

React 提供了错误边界组件,它可以捕获其子组件任何位置的 JS 错误,并且会渲染出备用 UI。如果在 class 组件中定义了static getDerivedStateFromError()或者componentDidCatch()这两个方法中任何一个时,该组件就变成了错误边界。

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}

// Vue 3.x
app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
}

请求错误监控

前端请求有两种方案 ajax 或者 fetch ,只需重写两种方法,进行代理,即可实现请求错误监控。

代理的核心在于使用 apply 重新执行原有方法,并且在执行原有方法之前进行监听操作。

在请求错误监控中,我们关心三种错误事件:aborterror 以及 timeout,所以,只需在代理中对这三种事件进行统一处理即可。

ps:可以使用请求工具如 axios ,不需要重写 ajax 或者 fetch 只需在请求拦截器以及响应拦截器进行处理上报即可。

资源错误监控

资源错误一般是指页面上的静态资源路径错误,如图片src。可以通过监控error错误事件实现错误捕获。

我们可以通过 instanceof 区分脚本错误和资源错误,脚本错误参数对象 instanceof ErrorEvent,而资源错误的参数对象 instanceof Event

由于 ErrorEvent 继承于 Event ,所以不管是脚本错误还是资源错误的参数对象,它们都 instanceof Event,所以,需要先判断脚本错误。此外,两个参数对象之间有一些细微的不同,比如,脚本错误的参数对象中包含 message ,而资源错误没有,这些都可以作为判断资源错误或者脚本错误的依据。

/**
 * @param event 事件名
 * @param function 回调函数
 * @param useCapture 回调函数是否在捕获阶段执行,默认是false,在冒泡阶段执行
 */
window.addEventListener('error', (event) => {
  if (event instanceof ErrorEvent) {
    console.log('脚本错误')
  } else if (event instanceof Event) {
    console.log('资源错误')
  }
}, true);

Ps:使用 addEventListener 捕获资源错误时,一定要将 useCapture 即第三个选项设为 true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。同理,由于 window.onerror 是通过在冒泡阶段捕获错误,所以无法捕获资源错误。

跨域脚本错误捕获

为了性能方面的考虑,我们一般会将脚本文件放到 CDN ,这种方法会大大加快首屏时间。但是,如果脚本报错,此时,浏览器出于于安全方面的考虑,对于不同源的脚本报错,无法捕获到详细错误信息,只会显示 Script Error。那么,有解决该问题的方案吗?

  1. 方案一:所有的脚本全部放到同一源下,但是,该方案会放弃 CDN ,降低性能。(可以使用Nginx做代理)
  2. 方案二:在 script 标签中,添加 crossorigin 属性(推荐使用 webpack 插件自动添加);同时,配置 CDN 服务器,为跨域脚本配上 CORS

方案二基本可以完美解决跨域脚本错误捕获的问题。但是,crossorigin 属性对于 IE 以及 Safari 支持程度不高。

错误上报方式

采用Ajax上报、使用image对象模拟图片请求方式上报。

使用Ajax请求上报,要对错误进行防抖操作,避免频繁上报重复错误。

一般来说,大厂都是采用利用image对象的方式上报错误的;使用图片发送get请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报。

var entry = {};
function report(url, data) {
    if (!url || !data) {
        return;
    }
    // @see http://jsperf.com/new-image-vs-createelement-img
    var image = document.createElement('img');
    var items = [];
    for (var key in data) {
        if (data[key]) {
            items.push(key + '=' + encodeURIComponent(data[key]));
        }
    }
    var name = 'img_' + (+new Date());
    entry[name] = image;
    image.onload = image.onerror = function () {
      console.log(arguments);
        entry[name] =
            image =
            image.onload =
            image.onerror = null;
        delete entry[name];
    };
    image.src = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');
}

错误上报需要的数据内容有:时间、message、错误类型、错误文件路径、source-map文件等。

另外,前台可建立数据可视化平台,对错误进行实时监控。