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文件等。

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

WEB漏洞攻击XSS、CSRF、SSRF的概念和防御

2022
23/09

XSS,CSRF,SSRF三种常见的Web服务端漏洞均是由于,服务器端对用户提供的可控数据过于信任或者过滤不严导致的。

XSS,CSRF,SSRF区别

XSS:(跨站脚本攻击)是服务器对用户输入的数据没有进行足够的过滤,导致客户端浏览器在渲染服务器返回的html页面时,出现了预期值之外的脚本语句被执行。通常指黑客通过 HTML 注入 篡改网页,插入恶意脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击行为。

CSRF:(跨站请求伪造)是服务器端没有对用户提交的数据进行校验,且对http请求包内的refer字段校验不严,导致攻击者可以利用用户的Cookie信息伪造用户请求发送至服务器。

SSRF:(服务端请求伪造)是服务器对用户提供的可控URL过于信任,没有对攻击者提供的URL进行地址限制和足够的检测,导致攻击者可以以此为跳板攻击内网或其他服务器。

xss防御

大部分XSS(跨站脚本攻击)的目的都是通过浏览器的同源策略,来获取用户Cookie,从而冒充用户登陆系统的。

可以使用 HttpOnly 来禁止JavaScript读取Cookie值,窃取用户身份(在setcookie方法的第七个传入参数中设置了变量true,表示这个cookie具有Http-only属性,那么任何JavaScript脚本都没有权限读取这条cookie的内容。)

一般在后台通过http设置前台cookie时添加此参数。如下为java设置方式:

response.setHeader("Set-Cookie","deniro=1; Path=/;Domain=www.deniro.net;"+"Max-Age=30;HTTPOnly");

只为cookie中的值设置HttpOnly 是不够的,因为XSS攻击并不是只能获取用户COOKIE,它的攻击类型有:

  • 利用xss获得cookie
  • 重定向到钓鱼网站
  • 展示不法内容或广告

防御措施有:

  • 对字符实体进行转义
  • 使用HttpOnly 来禁止JavaScript读取Cookie值
  • 输入时校验
  • 浏览器与Web应用端采用相同的字符编码
  • 对URL参数进行过滤(白名单和黑名单)
  • 如果 <script>, <style>, <img> 等标签的 src 和 href 属性值为动态内容,那么要确保这些url没有执行恶意连接。 确保:href 和 src 的值必须以 http://开头,白名单方式

csrf防御

Referer标识当前请求的来源页面,浏览器访问时除了自动带上Cookie还会自动带上Referer,所以服务端可以检测Referer头是否本网站页面来决定是否响应请求。

token就是服务端返回给客户端类似sessionid那样一长串的类值(长是为了防暴力猜解)。csrf依赖于浏览器该问链接时自动对应网站的cookie带上,token可以不存道cookie中,这样我们就可以通过检测发送过来的数据包中是否有正确的token值来决定是否响应请求。

ssrf防御

  • 限制协议为HTTPS,安装SSL证书,对网站传输加密数据
  • 不用限制302重定向
  • 设置URL白名单或者限制内网IP

其他安全措施

  • 服务器及时安装系统补丁
  • 服务器安装和设置防火墙
  • 关闭不需要的服务和端口,设置白名单
  • 定期对服务器进行备份
  • 设置监控机制和回滚机制,日志监控
  • 账号和密码保护(加密措施、验证码措施、密码复杂度)
  • 请求数据对称加密

Babel原理,SWC和ESBuild优势,Polyfill作用

2022
23/09

ECMAScript 发展困境

JavaScript这门语言受限于历史因素,有许多问题,其中一个就是新的语言特性始终无法在浏览器中得到全面支持与应用。

如果你要写一个支持绝大多数浏览器的网页,最好的选择可能就是ES5了,但你要知道ES5之后,JavaScript也一直在不停的更新,其中最重要的一个版本是ES 6,而到今年2022年,已经是ES 2022(ES13)了

和其它语言不同,JavaScript是要在浏览器中运行,这就决定了你在使用JS时不得不考虑浏览器的支持情况。

什么是Babel

Babel 是一个 JavaScript 编译器。主要用于将采用 ES 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。比如:

  • 语法转换(ES6转ES5,转换 JSX 语法)
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js
  • 由于 Babel 支持 Source map,因此你可以轻松调试编译后的代码。

Babel原理

babel的转移过程分为三个阶段,这三个步骤分别是:

  1. 解析(parse):将代码解析生成抽象语法树(AST),先词法分析语法分析的过程,最终转为AST。 使用 @babel/parser 解析代码,对不同词法添加不同type。
  2. 转换(Transform):对于AST进行变换的一些列的操作,babel接收得到的AST并通过babel(遍历)的相关插件babel-traverse对其进行遍历,在此过程中进行添加,更新以及移除等操作。
  3. 生成(Generate):将变换后的AST再转换为JS代码,转换成字符串形式的代码,同时还会创建源码映射(source maps),使用到的模块是babel-generator

什么是Polyfill

polyfill 指的是“用于实现浏览器不支持原生功能的代码”。

比如,现代浏览器应该支持 fetch 函数,对于不支持的浏览器,网页中引入对应 fetch 的 polyfill 后,这个 polyfill 就给全局的window对象上增加一个fetch函数,让这个网页中的 JavaScript 可以直接使用 fetch 函数了,就好像浏览器本来就支持 fetch 一样。

Babel中的Polyfill作用

Babel 包含一个 core-js 的 polyfill。默认情况下babel可以将箭头函数,class等语法转换为ES5兼容的形式,但是却不能转换Map,Set,Promise等新的全局对象和一些新的API,这时候就需要使用polyfill去模拟这些新特性。

新的API有Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign,Array.from,Array.prototype.includes)都不会转码。

使用babel-polyfill,可以为当前环境提供相应API的垫片。

什么是SWC

SWC(Speedy Web Compiler)是用 Rust 编写的超快 TypeScript / JavaScript 编译器。是一个社区驱动的项目。

SWC 的编译旨在支持所有 ECMAScript 功能。SWC CLI 旨在替代 Babel。可以说是更快的babel。

SWC相比于Babel的优势

Babel是JavaScript写的,JavaScript就是有点慢。而swc也提供了对 webpack 良好支持,所以使用webpack + swc搭配没有任何问题。

什么是esbuild

esbuild是由go语言写的。而最近比较流行的Vite工具,它就是使用的esbuild,所以它非常快。

ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

  1. esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
  2. go是纯机器码,肯定要比JIT快
  3. 不使用 AST,优化了构建流程。

使用 esbuild 去做一些代码的 transform (代替 babel-loader)。

SWC与esbuild

如果是这样,我们都很容易会去思考一个问题,如果JavaScript能写一个转换器,为什么不用一些更高性能的语言来写一个类似的转换器呢?

好想法,这也是为什么会有swc与esbuild 出现的原因。babel是开创性的,但这不表示我们不能有新的东西取代它,技术就是如此,新的取代旧的,这是永恒不变的趋势。

Vue3.x 与 Vue2.x 插槽区别

2022
07/09

1、基本用法

基本用法基本是相同的,没有什么改动,vue3和vue2都可以这样用

2、具名插槽

具名插槽两者用法有稍微不同,Vue3使用v-slot:[name](缩写#[name]),而Vue2用的是slot="name"

vue3写法

vue2写法

这里有两点需要注意

  • vue3在父组件中使用具名插槽使用v-slot,而vue2使用slot
  • vue3必须把v-slot写在template标签中,而vue2中的slot可以写在任意标签中

3、作用域插槽

经常我们会想让父组件的slot能够访问子组件的数据。

vue3

vue2

有关Vue3.x插槽的更多写法详见文档