返回 blog
2020年12月22日
3 分钟阅读

网页性能监控及优化点

性能监视

可以使用 window.performance

Web Performance API允许网页访问某些函数来测量网页和Web应用程序的性能,包括 Navigation Timing API和高分辨率时间数据。

这个API有个timing属性(实验室版本为timeOrigin), 其中包含了 image.png 可以通过

const time = new Date() - window.perfermance.timing.xxx

获取各种时间差,从而判断性能

timing解释

timing: {
  // 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同。
	navigationStart: 1543806782096,

	// 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。
	unloadEventStart: 1543806782523,

	// 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。
	unloadEventEnd: 1543806782523,

	// 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。
	redirectStart: 0,

	// 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。
	// 如果没有重定向,或者重定向中的一个不同源,这个值会返回0. 
	redirectEnd: 0,

	// 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
	fetchStart: 1543806782096,

	// DNS 域名查询开始的UNIX时间戳。
        //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。
	domainLookupStart: 1543806782096,

	// DNS 域名查询完成的时间.
	//如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
	domainLookupEnd: 1543806782096,

	// HTTP(TCP) 域名查询结束的时间戳。
        //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。
	connectStart: 1543806782099,

	// HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。
        // 如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
	connectEnd: 1543806782227,

	// HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。
	secureConnectionStart: 1543806782162,

	// 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
	requestStart: 1543806782241,

	// 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。
        //如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。
	responseStart: 1543806782516,

	// 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时
        //(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。
	responseEnd: 1543806782537,

	// 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。
	domLoading: 1543806782573,

	// 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。
	domInteractive: 1543806783203,

	// 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。
	domContentLoadedEventStart: 1543806783203,

	// 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。
	domContentLoadedEventEnd: 1543806783216,

	// 当前文档解析完成,即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳
	domComplete: 1543806783796,

	// load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。
	loadEventStart: 1543806783796,

	// 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.
	loadEventEnd: 1543806783802
}

性能检测

可以使用chrome浏览器自带的 lightHouse 检测网站性能,并且针对结果进行优化 image.png image.png

  • perfermance : 性能评分
    • 性能评分主要参考 [首屏第一个dom渲染时间](https://web.dev/first-contentful-paint/) 、 [可交互时间](https://web.dev/interactive/) 、 布局偏移量 、 [速度指数](https://web.dev/speed-index/)   、 [总阻塞时间](https://web.dev/lighthouse-total-blocking-time/) 、 首屏总渲染时间 
    • image.png
  • accessibility : 可访问性
    • 无障碍访问
  • bast practices: JS最佳实践
    • 怎么写规范的JS, 比如最好不要使用HTTP链接, 第三方包隐私问题等

性能上报

性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。

window.onload = () => {
    // 在浏览器空闲时间获取性能及资源信息
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    if (window.requestIdleCallback) {
        window.requestIdleCallback(() => {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        })
    } else {
        setTimeout(() => {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        }, 0)
    }
}

错误上报

现在能捕捉的错误有三种。

  1. 资源加载错误,通过 addEventListener('error', callback, true) 在捕获阶段捕捉资源加载失败错误。
  2. js 执行错误,通过 window.onerror 捕捉 js 错误。
  3. promise 错误,通过 addEventListener('unhandledrejection', callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。

可以创建一个error数组,每次发生报错就把错误push到数组中,统一上报

性能优化

性能优化分为两种,

  1. 加载时优化,如 webpack打包处理、cdn、图片压缩、路由懒加载等。具体优化方式可见我另外一篇文章
  2. 运行时优化,如减少dom操作,事件委托,优化if else等

反思自己,很少用到事件委托, if else优化也仅仅停留在转换成switch case上。 差得太远了

关于性能优化,参考这篇文章

总结一下我还没有用到的性能优化:

  1. 使用iconfont代替svg图标。由于现在的开发模式就是在ui上切图,导致没有统一的iconfont库
  2. 善用缓存,不重复加载相同的内容。 由于项目特殊,经常更新,所以在index.html中设置了meta no-cache。如果需要利用缓存功能的话,就需要在服务端配置cache-control以及max-age之类的
  3. 图片模糊加载,我在另外一篇文章里面有说过我对这个功能的思考,有利有弊,弊端在于会增加http请求,利就是用户体验更好了。不过知乎对图片加载的处理方式是用了一个占位蒙层占在图片的位置上,等图片加载后,这个蒙层透明图再变成0、filter也变成0,就有种好像是模糊加载的感觉了
  4. 事件委托。 在很多地方都能用到事件委托,而我总是把事件放在了每个item上。
  5. if else优化:使用策略模式。
// 搞个比较真实的
api().then(res=>{
	if(res.code === 0) {
  	console.log('0嗷')
  } else if(res.code === 1) {
  	console.log('1嗷')
  } else if(res.code === 100) {
  	console.log('1=>100很简单嗷')
  } else if(res.code === 1000) {
  	console.log('1000嗷')
  }
  ...
})
...

当if else越来越多时,代码的可读性和维护性都不好。可以改成如下:

const strategy = {  // 一组策略
 	'0': function(code) {
    // 业务代码
  	console.log(code)
  },
  '1': function(code) {
  	console.log(code)
  },
  '100': function(code) {
  	console.log(code)
  },
  '1000': function(code) {
  	console.log(code)
  },
  '2000': function(code) { // 如果又增加了else情况,加一个对象就行了。很清晰
  	console.log(code)
  }
}

api().then(res=>{
	strategy[res.code](250)
})

这种方式还是有局限性的,好像只能去搞一些简单的if else嘛, 如果if的条件判断比较复杂呢?

api().then(res=>{
	if(res.code > 1 && res.code < 100) {
  	console.log(1)
  } else if(res.code > 100 && res.code < 100) {
  	console.log(2)
  } else if(res.data === '??' && res.code === 0) {
  	console.log(3)
  }
})

这种算是比较复杂的了。改成策略模式

// 首先,得知道map长什么样子
const map = new Map([[key,val],[key,val]])
// map大概就长这个样子。 key可以是任意值。 具体见mdn

const strategy = new Map([
	[
    (code, data) => code > 1 && code < 100, 
    () => {
      console.log(1)
    }	
  ],
  [
    (code, data) => code > 100 && code < 1000,
   	() => {
      console.log(2)
    }
  ],
  [
  	(code, data) => data === '??' && code === 0,
    () => {
    	console.log(3)
    }
  ]
])

function handle(res) {
  // 首先要总结 if else 中所使用的变量: res.code, res.data  两个变量
	// 其次,由于判断条件复杂,那么key自然而然是个回调函数,所以我想到了使用Map
  // 循环map。 可以使用forEach, 也可以for...of
  for(const [condition, method] of strategy) {
  	condition(res.code, res.data) && method()
  }
}

// 改造之后的
api().then(res=>{
	handle(res)
})
  1. 使用位操作: ~~1.1 => 1 取整