工程化体系专栏永远首发自个人 Github,你们能够关注点赞,一般会早于发布各大平台一周时间以上。
本文涉及到的源码及视频地址:前端
常常有读者问我什么是前端工程化?该怎么开始作前端工程化?git
聊下来之后得出一些结论:这类读者广泛就任于中小型公司,前端人员个位数,平时疲于开发,团队内部几乎没有基础建设,工具很蛮荒。工程化对于这些读者来讲很陌生,基本不知道这究竟是什么,或者说认为 Webpack 就是前端工程化的所有了。github
笔者目前就任于某厂的基础架构组,为百来号前端提供基础服务建设,对于这个领域有些许皮毛经验。所以有了一些想法,前端搞工程化会是笔者今年开坑的一个系列做品,每块内容会以文章 + 源码 + 视频的方式呈现。web
这个系列的产出适用于如下群体:面试
须要说明的是产出只会是一个低成本下的最小可用产品,你能够拿来按需增长功能、参考思路或者纯粹当学习一点知识。前端工程化
由于是该系列第一篇文章,就先来大体说下什么是前端工程化。浏览器
个人理解是前端工程化大致上能够理解为是作提效工程,从写代码开始的每一步均可以作工程化。好比说你用 IDE 对比记事本写代码的体验及效率确定是不同的;好比说 Webpack 等这类工具也是在帮助咱们提高开发、构建的效率,其余的工具也就不一一列出了,你们知道意思就好。性能优化
固然了,今天要聊到的性能检测也是工程化的一部分。毕竟咱们须要有个工具去协助找到应用到底在哪块地方存在性能短板,能帮助开发者更快地定位问题,而不是在生产环境中让用户抱怨产品卡顿。网络
性能优化是不少前端都绕不开的话题,先不说项目是否须要性能优化,面试的时候这类问题是很常见的。数据结构
可是光会性能优化的手段仍是不够的,咱们最后仍是须要作出先后数据对比才能体现出此次优化的价值到底有多少,毕竟数据的量化在职场中仍是至关重要的。老板不知道你具体作的事情,不少东西都得从数据中来看,数据越好看就说明你完成工做的能力越高。
想获取性能的先后数据变化,咱们确定得用一些工具来作性能检测。
性能检测的方式有不少:
这些方法各有各的好处,前两种方式简单快捷,可以可视化各种指标,可是很难拿到用户端的数据,毕竟你不大可能让用户去跑这些工具,固然除此以外还有一些很小的缺点,好比说拿不到重定向次数等等。
官方库、插件相比前二者来讲会逊色不少,而且只提供一部分核心指标。
原生 Performance API 存在兼容问题,可是能覆盖到开发生产阶段,而且功能也能覆盖自带的开发者工具:Performance 工具。不只在开发阶段能了解到项目的性能指标,还能获取用户端的数据,帮助咱们更好地制定优化方案。另外能获取的指标也很齐全,所以是这次咱们产品的选择。
固然了这不是多选一的选择题,咱们在开发阶段仍是须要将 Performance 工具及 API 结合起来使用,毕竟他们仍是有着相辅相成的做用。
这是此处产品的源码:地址。
在开始实战前,咱们仍是得来了解一些性能指标,随着时代发展,其实一些老的性能优化文章已经有点过期了。谷歌一直在更新性能优化这块的指标,笔者以前写过一篇文章来说述当下的最新性能指标有哪些,有兴趣的读者能够先详细的读一下。
固然若是你嫌太长不看,能够先经过如下思惟导图简单了解一下:
固然除了这个指标之外,咱们还须要获取网络、文件传输、DOM等信息丰富指标内容。
Performance
接口能够获取到当前页面中与性能相关的信息,而且提供高精度的时间戳,秒杀 Date.now()
。首先咱们来看下这个 API 的兼容性:
这个百分比其实已经算是兼容度很高了,主流浏览器的版本都能很好的支持。
对于 Performance 上 API 具体的讲解文中就不赘述了,有兴趣的能够阅读 MDN 文档,笔者在这里只讲几个后续用到的重要 API。
这个 API 可让咱们经过传入 type
获取一些相应的信息:
performance.mark
调用信息performance.measure
调用信息最后两个 type
是性能检测中获取指标的关键类型。固然你若是还想分析加载资源相关的信息的话,那能够多加上 resource
类型。
PerformanceObserver
也是用来获取一些性能指标的 API,用法以下:
const perfObserver = new PerformanceObserver((entryList) => { // 信息处理 }) // 传入须要的 type perfObserver.observe({ type: 'longtask', buffered: true })
结合 getEntriesByType
以及 PerformanceObserver
,咱们就能获取到全部须要的指标了。
由于已经贴了源码地址,笔者就不贴大段代码上来了,会把主要的从零到一过程梳理一遍。
首先咱们确定要设计好用户如何调用 SDK(代指性能检测库)?须要传递哪些参数?如何获取及上报性能指标?
通常来讲调用 SDK 可能是构建一个实例,因此此次咱们选择 class
的方式来写。参数的话暂定传入一个 tracker
函数获取各种指标以及 log
变量决定是否打印指标信息,签名以下:
export interface IPerProps { tracker?: (type: IPerDataType, data: any, allData: any) => void log?: boolean } export type IPerDataType = | 'navigationTime' | 'networkInfo' | 'paintTime' | 'lcp' | 'cls' | 'fid' | 'tbt'
接下来咱们写 class
内部的代码,首先在前文中咱们知道了 Performance API 是存在兼容问题的,因此咱们须要在调用 Performance 以前判断一下浏览器是否支持:
export default class Per { constructor(args: IPerProps) { // 存储参数 config.tracker = args.tracker if (typeof args.log === 'boolean') config.log = args.log // 判断是否兼容 if (!isSupportPerformance) { log(`This browser doesn't support Performance API`) return } } export const isSupportPerformance = () => { const performance = window.performance return ( performance && !!performance.getEntriesByType && !!performance.now && !!performance.mark ) }
以上前置工做完毕之后,就能够开始写获取性能指标数据的代码了。
咱们首先经过 performance.getEntriesByType('navigation')
来获取关于文档事件的指标
这个 API 仍是能拿到挺多事件的时间戳的,若是你想了解这些事件具体含义,能够阅读文档,这里就不复制过来占用篇幅了。
看到那么多字段,可能有的读者就晕了,那么多东西我可怎么算指标。其实不须要担忧,看完下图结合刚才的文档就好了:
咱们不须要所有利用上得到的字段,重要的指标信息暴露出来便可,照着图和文档依样画葫芦就能得出代码:
export const getNavigationTime = () => { const navigation = window.performance.getEntriesByType('navigation') if (navigation.length > 0) { const timing = navigation[0] as PerformanceNavigationTiming if (timing) { // 解构出来的字段,太长不贴 const {...} = timing return { redirect: { count: redirectCount, time: redirectEnd - redirectStart, }, appCache: domainLookupStart - fetchStart, // dns lookup time dnsTime: domainLookupEnd - domainLookupStart, // handshake end - handshake start time TCP: connectEnd - connectStart, // HTTP head size headSize: transferSize - encodedBodySize || 0, responseTime: responseEnd - responseStart, // Time to First Byte TTFB: responseStart - requestStart, // fetch resource time fetchTime: responseEnd - fetchStart, // Service work response time workerTime: workerStart > 0 ? responseEnd - workerStart : 0, domReady: domContentLoadedEventEnd - fetchStart, // DOMContentLoaded time DCL: domContentLoadedEventEnd - domContentLoadedEventStart, } } } return {} }
你们能够发现以上得到的指标中有很多是和网络有关系的,所以咱们还须要结合网络环境来分析,获取网络环境信息很方便,如下是代码:
export const getNetworkInfo = () => { if ('connection' in window.navigator) { const connection = window.navigator['connection'] || {} const { effectiveType, downlink, rtt, saveData } = connection return { // 网络类型,4g 3g 这些 effectiveType, // 网络下行速度 downlink, // 发送数据到接受数据的往返时间 rtt, // 打开/请求数据保护模式 saveData, } } return {} }
拿完以上的指标以后,咱们须要用到 PerformanceObserver
来拿一些核心体验(性能)指标了。好比说 FP、FCP、FID 等等,内容就包括在咱们上文中看过的思惟导图中:
在这以前咱们须要先了解一个注意事项:页面是有可能在处于后台的状况下加载的,所以这种状况下获取的指标是不许确的。因此咱们须要忽略掉这种状况,经过如下代码来存储一个变量,在获取指标的时候比较一下时间戳来判断是否处于后台中:
document.addEventListener( 'visibilitychange', (event) => { // @ts-ignore hiddenTime = Math.min(hiddenTime, event.timeStamp) }, { once: true } )
接下来是获取指标的代码,由于他们获取方式大同小异,因此先把获取方法封装一下:
// 封装一下 PerformanceObserver,方便后续调用 export const getObserver = (type: string, cb: IPerCallback) => { const perfObserver = new PerformanceObserver((entryList) => { cb(entryList.getEntries()) }) perfObserver.observe({ type, buffered: true }) }
咱们先来获取 FP 及 FCP 指标:
export const getPaintTime = () => { const data: { [key: string]: number } = ({} = {}) getObserver('paint', entries => { entries.forEach(entry => { data[entry.name] = entry.startTime if (entry.name === 'first-contentful-paint') { getLongTask(entry.startTime) } }) }) return data }
拿到的数据结构长这样:
须要注意的是在拿到 FCP 指标之后须要同步开始获取 longtask 的时间,这是由于后续的 TBT 指标须要使用 longtask 来计算。
export const getLongTask = (fcp: number) => { getObserver('longtask', entries => { entries.forEach(entry => { // get long task time in fcp -> tti if (entry.name !== 'self' || entry.startTime < fcp) { return } // long tasks mean time over 50ms const blockingTime = entry.duration - 50 if (blockingTime > 0) tbt += blockingTime }) }) }
接下来咱们来拿 FID 指标,如下是代码:
export const getFID = () => { getObserver('first-input', entries => { entries.forEach(entry => { if (entry.startTime < hiddenTime) { logIndicator('FID', entry.processingStart - entry.startTime) // TBT is in fcp -> tti // This data may be inaccurate, because fid >= tti logIndicator('TBT', tbt) } }) }) }
FID 的指标数据长这样,须要用户交互才会触发:
在获取 FID 指标之后,咱们也去拿了 TBT 指标,可是拿到的数据不必定是准确的。由于 TBT 指标的含义是在 FCP 及 TTI 指标之间的长任务阻塞时间之和,但目前好像没有一个好的方式来获取 TTI 指标数据,因此就用 FID 暂代了。
最后是 CLS 和 LCP 指标,大同小异就贴在一块儿了:
export const getLCP = () => { getObserver('largest-contentful-paint', entries => { entries.forEach(entry => { if (entry.startTime < hiddenTime) { const { startTime, renderTime, size } = entry logIndicator('LCP Update', { time: renderTime | startTime, size, }) } }) }) } export const getCLS = () => { getObserver('layout-shift', entries => { let cls = 0 entries.forEach(entry => { if (!entry.hadRecentInput) { cls += entry.value } }) logIndicator('CLS Update', cls) }) }
拿到的数据结构长这样:
另外这两个指标还和别的不大同样,并非一成不变的。一旦有新的数据符合指标要求,就会更新。
以上就是咱们须要获取的全部性能指标了,固然光获取到指标确定是不够,还须要暴露每一个数据给用户,对于这种统一操做,咱们须要封装一个工具函数出来:
// 打印数据 export const logIndicator = (type: string, data: IPerData) => { tracker(type, data) if (config.log) return // 让 log 好看点 console.log( `%cPer%c${type}`, 'background: #606060; color: white; padding: 1px 10px; border-top-left-radius: 3px; border-bottom-left-radius: 3px;', 'background: #1475b2; color: white; padding: 1px 10px; border-top-right-radius: 3px;border-bottom-right-radius: 3px;', data ) } export default (type: string, data: IPerData) => { const currentType = typeMap[type] allData[currentType] = data // 若是用户传了回调函数,那么每次在新获取指标之后就把相关信息暴露出去 config.tracker && config.tracker(currentType, data, allData) }
封装好函数之后,咱们能够这样调用:
logIndicator('FID', entry.processingStart - entry.startTime)
在这里为止咱们 SDK 的大致内容已经完成了,咱们能够按需添加一些小功能,好比说获取指标分数。
指标分数是官方给的一些建议,你能够在官方 Blog 或者个人文章中看到定义的数据。
代码不复杂,咱们就以获取 FCP 指标的分数为例演示一下代码:
export const scores: Record<string, number[]> = { fcp: [2000, 4000], lcp: [2500, 4500], fid: [100, 300], tbt: [300, 600], cls: [0.1, 0.25], } export const scoreLevel = ['good', 'needsImprovement', 'poor'] export const getScore = (type: string, data: number) => { const score = scores[type] for (let i = 0; i < score.length; i++) { if (data <= score[i]) return scoreLevel[i] } return scoreLevel[2] }
首先是获取分数相关的工具函数,这块反正就是看着官方建议照抄,而后咱们只须要在刚才获取指标的地方多加一句代码便可:
export const getPaintTime = () => { getObserver('paint', (entries) => { entries.forEach((entry) => { const time = entry.startTime const name = entry.name if (name === 'first-contentful-paint') { getLongTask(time) logIndicator('FCP', { time, score: getScore('fcp', time), }) } else { logIndicator('FP', { time, }) } }) }) }
结束了,有兴趣的能够来这里读一下源码,反正也没几行。
文章周末写的,略显仓促,若有出错请斧正,同时也欢迎你们一块儿探讨问题。
想看更多文章能够关注个人 Github 或者进群一块儿聊聊前端工程化。