window.performance
memory
- jsHeapSizeLimit (表示当前页面最多可以获得的 JavaScript 堆大小)
- totalJSHeapSize (表示当前页面已经分配的 JavaScript 堆大小)
- usedJSHeapSize (表示当前页面 JavaScript 已经使用的堆大小)
navigation
- redirectCount,重定向的数量,经过多少次重定向进入这个页面
- type,进入页面的方式
- 0,正常进入非刷新,非重定向
- 1,通过刷新的方式进入
- 2,通过前进回退按钮进入
- 255,非是上述情况
PerformanceNavigationTiming
- 顺序
- 页面卸载部分
- 卸载:navigationStart、unloadEventStart、unloadEventEnd
- 网络部分
- 跳转:redirectStart、redirectEnd
- 请求文档:fetchStart
- DNS查询:domainLookupStart、domainLookupEnd
- 连接建立:connectStart、connectEnd
- 请求:requestStart、responseStart、responseEnd
- 页面解析部分
- 解析dom:domLoading、domInteractive
- 资源加载,dom渲染:domContentLoadedEventStart、domContentLoadedEventEnd、domComplete
- 页面卸载部分
参考
- PerformanceNavigationTiming - Web APIs | MDN
- 来来来,前端性能监控,带你拿到正确的性能指标作为前端开发,我们有必要关注我们各个客户端的性能指标,比如白屏时间,最早可交 - 掘金
指标
首次内容绘制(First Contentful Paint)
- 定义
- 浏览器从响应用户输入网络地址,到浏览器呈现第一段 DOM 内容所需的时间
- 首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点
- performance.getEntries(‘paint’).filter(entry => entry.name == ‘first-contentful-paint’)[0].startTime;
- 优化
- 缩短距离: cdn
- 减少首次加载需要请求的内容
- 动态加载: 使用 import() 或 React.lazy() 在路由或组件级别进行模块拆分
- 懒加载:
intersectionObserver
最大内容渲染时间(LCP Largest Contentful Paint)
- 定义
- 指代页面中最大的内容完成绘制的时间。
TTFB
- 定义
- 指代从资源的请求到响应第一个字节的时间跨度。
- responseStart - navigationStart
- 影响因素
- 重定向时延,DNS查询时延,链接建立延迟,请求响应时延
可交互时间(Time to Interactive)
- 定义
- 页面完全交互所需的时间
- 优化
- 把长任务拆分为较小的任务(使用 requestAnimationFrame 或 requestIdleCallback),或者选择在主线程之外(如Web Workers)运行 JavaScript
DCL (DOMContentLoaded)
- 定义
- 当HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式,图像和子框架的完成加载的时间。
- 计算
document.addeventListener('DOMContentLoaded', function() {
+new Date() - window.performanceObser.timing.navigationStart
}, false);
(Total Blocking Time)
(Cumulative Layout Shift)
白屏时间
- 从用户开始请求页面时开始计算到开始显示内容结束,中间过程包括DNS查询、建立TCP链接、发送首个HTTP请求、返回HTML文档、HTML文档head解析完毕。
- 计算:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>白屏时间计算-常规方法</title> <script> window.pageStartTime = Date.now() </script> <link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/ionicons/2.0.1/css/ionicons.min.css~tplv-t2oaga2asx-image.image"> <link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/asset/fw-icon/1.0.9/iconfont.css~tplv-t2oaga2asx-image.image"> <script> window.firstPaint = Date.now() console.log(`白屏时间:${window.firstPaint - window.pageStartTime}`) </script> </head> <body> <div>这是常规计算白屏时间的示例页面</div> </body> </html>
- 白屏时间 = window.firstPaint - window.pageStartTime
参考
- 看懂 Lighthouse 中 Performance 核心指标简介
- 前端性能指标:白屏和首屏时间的计算 - 掘金
- How the Core Web Vitals metrics thresholds were defined | Articles | web.dev
首屏优化
滚屏性能优化
- 将
touchstart
和touchmove
事件的passive
选项设置为ture- 原理:
touchstart
和touchmove
是cancelable
的。事件处理程序中可能会调用preventDefault()
,所以需要等待事件处理程序结束再滚动。 - 注: 大部分浏览器(Safari 和 Internet Explorer 除外)将文档级节点 Window、Document 和 Document.body 上的 wheel、mousewheel、touchstart 和 touchmove 事件的 passive 默认值更改为 true。
- 原理:
- 参考:
React性能优化
- map循环展示添加key
- 路由懒加载
- 第三方依赖按需引入
- 使用memo或者pureComponent避免不必要的渲染
- 合理使用useMemo、memo、useCallback
参考
- React 性能优化 | 包括原理、技巧、Demo、工具使用React 的工作流 - 掘金
- 当面试官问我前端可以做的性能优化有哪些-网络日志
- React 应用中的性能优化 | 前端开发者技术周刊
- React 性能优化 | 五年前端三年面试
- React 性能优化十大总结
懒加载与按需加载
懒加载
- 概念: 可以按照路由对产物进行分割
- 实现
import { lazy } from 'react'; const MarkdownPreview = lazy(() => import('./MarkdownPreview.js')); <Suspense fallback={<Loading />}> <h2>Preview</h2> <MarkdownPreview /> </Suspense>
- 代码二次分割:
- 可以将那些在非首页才用到的依赖是不是可以单独分割为一个chunk,这样的话可以进一步减少
index-[hash].js
的体积 - 需要配合路由懒加载才能达到效果,否则即使代码进行了分割,但加载首屏还是会因为路由提前注册的原因,使得浏览器加载所有资源。
- 可以将那些在非首页才用到的依赖是不是可以单独分割为一个chunk,这样的话可以进一步减少
- 缺点:
- 优化也有成本,比如文本中虽然通过代码分割+路由懒加载减少了首屏资源体积,但随之也带来了资源请求数增加的问题,对浏览器的并发请求带来了压力。
按需加载
- 按需加载是为了减少依赖产物体积过大的问题,典型的如 lodash、echarts、arco-design,整个包存在很多方法、组件,而在项目实际使用到的其实只有一部分。在构建时进行按需加载,可以有效减小最终产物体积。
- 示例:
- mattiasw/ExifReader: A JavaScript Exif info parser.
"exifreader": { "include": { "webp": true }, "exclude": { "xmp": true } }
- mattiasw/ExifReader: A JavaScript Exif info parser.
参考
虚拟滚动
定义
- 浏览器视口固定,内容响应用户鼠标滚动
使用场景
- 炫酷的滚动交互
- 无限循环列表滚动
- 关注控制滚动体验(灵敏度、惯性等)
如何优化解决虚拟滚动中滚速过快情况下的白屏问题
wip
参考
防抖和节流
防抖
- 在触发频率高的事件中,执行耗费性能的操作,连续操作后只有最后一次生效
//自己实现debounce function debounce(func,wait=0){ let timeId = null; //为什么不在内部声明id?因为我如果在内部声明,每一次触发事件就会有一个新的变量产生 return function(...args){ const self=this; if(timeId){ clearTimeout(timeId); } timeId=setTimeout(()=>{ func.apply(self,args); },wait) }; }
节流
- 在触发频率高的事件中,执行耗费性能操作,连续触发,单位时间内只有一次生效
function throttle(func,wait=0){ let timeId; return function(...args){ const self=this; if(!timeId){ func.apply(self,args); timeId = setTimeout(()=>{ timeId = undefined; },delay); } }; }
参考
回流与重绘
浏览器渲染过程
- 解析 HTML 和样式计算 (parsing and style calculation)
- 布局 (Layout)
- 绘制 (paint)
- 合成 (compositing)
回流
- reflow
- 网页渲染引擎根据元素的尺寸、位置和显示属性来重新计算页面的排版和布局,是网页渲染过程中的一个重要步骤。
- 当一个元素的长与宽改变,可能会影响到画面中其他元素的编排,所以每当有一个元素的布局改变,浏览器的 CPU 需要重新计算整个页面中不同元素的长宽、间距等。在计算的期间,会没办法处理其他任务 (例如使用者在点击按钮,可能会要等一阵子才有回应,因为浏览器无暇处理)。
重绘
- repaint
- 指网页渲染引擎根据显示属性 (如颜色、文字大小等) 重新绘制页面元素,不影响元素的位置和尺寸
- 不会影响页面布局,但是也会影响页面的性能
减少回流和重绘
- 移动调整元素时,使用
transform
- 使用
opacity
来改变元素的能见度 - 避免频繁用 JavaScript 操作 DOM 节点
参考
内存泄漏
定义
- 内存泄漏可以定义为程序不再使用或不需要的一块内存,但是由于某种原因没有被释放仍然被不必要的占有。
垃圾回收算法
- 引用计数: 如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
- 缺点: 循环引用
- 标记清除
- 过程
- 垃圾回收器创建了一个“roots”列表。Roots通常是代码中全局变量的引用。JavaScript中,“window”对象是一个全局变量,被当作root。window对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
- 所有的 roots被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从root开始的所有对象如果是可达的,它就不被当作垃圾。
- 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。
- 缺点: 算法运行时程序执行被暂停。
- 过程
常见的内存泄漏
- 全局变量
- 被遗忘的定时器和回调函数
- DOM引用
- 错误使用的闭包
排查
- 启用
use strict
- 浏览器开发者工具,内存选项
参考
- 彻底掌握js内存泄漏以及如何避免前言:内存泄漏写任何语言都必须得注意的问题,我司技术老大日常吐槽:以前做游戏的改内存泄漏 - 掘金
- JS 内存泄露问题该如何排查? · Issue #76 · pro-collection/interview-question
- JS的垃圾回收与内存泄漏 - 橙汁坤的博客 | czkm Blog
前端监控系统
wip
白屏检测
参考
- 目前为止整理最全的前端监控体系搭建篇(长文预警)-腾讯云开发者社区-腾讯云
- Application Performance Monitoring & Error Tracking Software | Sentry
- 前端性能优化–卡顿的监控和定位 | Here. There.
- 不可不知的前端工程化—— 埋点&监控 - 掘金
场景题
线上的某个页面加载慢,如何优化
wip
场景题: 针对大批量的数据的优化
- 问题: 如果data数据很大,可能导致内存溢出和Minor GC阻塞渲染进程的问题。垃圾回收过程会占用主线程资源,导致渲染进程被阻塞。
- 思想:
- 将数据分成多个较小的批次进行处理,而不是一次性处理整个数据集。这可以通过使用
setTimeout
或requestAnimationFrame
来分批处理数据。循环处理每个批次的数据,并在处理完一个批次后延迟一小段时间处理下一个批次.比如可以将数据分成每批100条,每批处理完后延迟10毫秒,以避免阻塞主线程。 - processBatch函数用于处理一批数据。它会根据当前的currentIndex从keys数组中选择一批数据进行处理,并将结果存入arr数组。然后,currentIndex增加,检查是否还有剩余数据需要处理。如果还有剩余数据,则通过requestAnimationFrame调度下一个异步任务来处理下一批数据。如果所有数据处理完成,则进行后续操作。
- 使用递归调用和requestAnimationFrame可以确保数据处理过程在异步任务中进行,在每一帧中调用 processBatch 函数,给渲染进程更多的时间进行渲染
- 一旦处理完所有数据,调用 finishProcessing 函数,并进行视图更新
- 将数据分成多个较小的批次进行处理,而不是一次性处理整个数据集。这可以通过使用
- 代码:
processTreeData(data) {
const keys = Object.keys(data);
let arr = [];
const batchSize = 100; // 每批处理的数据量
let currentIndex = 0;
const processBatch = () = >{
const batchKeys = keys.slice(currentIndex, currentIndex + batchSize);
const batchItems = batchKeys.flatMap((key) = >{
const tableData = data[key];
const parentId = `$ {key}. * `;
const childItems = tableData.map((item) = >({
id: `$ {key}.$ {item}`,
label: `$ {key}.$ {item}`,
parent: parentId
}));
return [{
id: parentId,
label: key,
children: childItems
}];
});
arr.push(...batchItems);
currentIndex += batchSize;
if (currentIndex < keys.length) {
requestAnimationFrame(processBatch);
} else {
this.finishProcessing(arr);
arr = [];
}
};
requestAnimationFrame(processBatch);
},
finishProcessing(arr) {
//finish operate
},
- 好处:
- 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。这样就会让用户觉得卡顿
- 可以显示进度指示
- 思路二:
- 对于不应该阻塞事件循环的耗时长的繁重计算任务,我们可以使用 Web Workers。
- Web Workers 可以与主线程交换消息,但是它们具有自己的变量和事件循环。
- 参考
项目中的分片上传,如何实现的?
wip
怎么做到组件的按需引入
- treeshaking