1.网站性能优化概述
我们通常的标量化
- 白屏
- 首屏
- 页面整体加载
- 页面可交互
- 功能交互响应
Steve Souders在《高性能网站建设指南》总结的12条提高性能的基本规则
- 尽量减少http请求
- 使用CDN
- 静态资源使用Cache
- 启用Gzip压缩
- JavaScript脚本尽量放在页面底部
- CSS样式表放在顶部
- 避免CSS表达式(也就是不要频繁动态改动CSS)
- 减少内联的CSS和JavaScript
- 减少DNS查询
- 精简JavaScript
- 避免重定向
- 删除重复脚本
2.白屏
白屏时长我们可以从URL地址的输入 -> 请求发出 -> 页面渲染完成,之中找到优化的方案
DNS -> TCP ->服务端响应 -> 客户端下载、解析、渲染显示页面
DNS Lookup
DNS的优化策略有很多
- DNS缓存优化
- DNS预加载策略
- 页面中资源域名的合理分配
- 稳定可靠的DNS服务器
客户端下载
具体步骤
- 如果是Gzip包,先解压为HTML
- 解析HTML头部代码,下载相关的资源(CSS、JS等)
- 正常的网页绘制流程(render树的合成以及页面的绘制)
而在真实的构件中,浏览器会因为各种因素被阻断
- JS代码会阻断DOM树的构造
- 浏览器必须等待样式表加载完成,采购件CSSOM树
- Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而Chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。(却决于浏览器的策略)
从上述我们可以发现HTML中内联JS代码的恶劣性质。它最大的危害在于第三种,即DOM树构建过程中被JS阻塞,而JS被CSS阻塞,这导致了CSSOM树和DOM树同时(并行)构建的理想状态被打破
然而对以上的优化策略,我们可以着手于
- 优化HTML代码结构,缩短下载时间 + 解析速度
- 优化CSS文件和结构
- 合理放置JS代码
网上关于CSR首屏优化的方案
链接https://github.com/BetaSu/fe-hunter/issues/4#issuecomment-1075856575
一、如何分析首屏加载CSR的性能?
有三种方式进行分析:
- network(浏览器->DevTools)
- performance (浏览器->DevTools)
- lighthouse(浏览器->DevTools)
- sentry(第三方平台)
- webPageTest 更全面,更详细,更强大的页面分析
具体怎么操作我就不赘述了,各位自行去看。
二、分析后如何找出CSR性能瓶颈?
按照上面的5种方式来看吧
三、如何解决CSR性能瓶颈?
从CSR方向来讲的话,基本就两个方向了
- 网络层面:
从网络方面来讲,就是减少HTTP请求的次数,以及减小请求体积,这里就简单聊几点
a). 将多次请求合并到一次请求(ajax、精灵图,文件合并等)
b). 页面懒加载应该也可以算在这个方面。
c). 缓存住一些不经常改动的文件或内容将请求拦截(service worker)
b). 文件内容压缩
e). 静态资源CDN也是方式之一
f). 升级到 http2 - 渲染层面
a). 骨架屏,起码能让用户有打开了网页的感觉
b). 图片懒加载
c). 防抖节流
d). 代码层面的改善吧,很有可能是某些坏代码产生的
四、有没有其他渲染方案可以解决CSR当前问题,他的原理是什么?
SSR。就是将框架代码提前在服务端跑一遍,不过一般除非对SEO要求比较高或者你们公司比较有钱,否则不太会去使用,因为比较占用服务器的资源。
3.页面整体加载完成
又被称为PageLoad,顾名思义所有页面相关资源全部加载完成的时间,而其中这个时间点为我们熟知的“8s”原则
随后在2006年,一个来自Akamai的新研究,发表了新的观点:通常4s左右的平均加载时间,是用户可能会等待页面加载的最长时间。
尽早建立第三方连接
对第三方域的服务器请求也会影响 LCP,尤其是当浏览器需要这些请求来在页面上显示关键内容的情况下。使用rel="preconnect"
来告知浏览器您的页面打算尽快建立连接。
<link rel="preconnect" href="https://example.com" />
您还可以使用dns-prefetch
来更快地完成 DNS 查找。
<link rel="dns-prefetch" href="https://example.com" />
尽管两种提示的原理不同,但对于不支持preconnect
的浏览器,可以考虑将dns-prefetch
做为后备。
<head>
…
<link rel="preconnect" href="https://example.com" />
<link rel="dns-prefetch" href="https://example.com" />
</head>
预加载重要资源 #
有时,在某个 CSS 或 JavaScript 文件中声明或使用的重要资源可能会比您所期望的要晚一点被获取,例如深藏在应用程序众多 CSS 文件中的某个字体。
如果您知道某个特定资源应该被优先获取,请使用<link rel="preload">
来更加及时地获取该资源。 多种类型的资源都可以进行预加载,但您应该首先侧重于预加载关键资产,例如字体、首屏图像或视频,以及关键路径 CSS 或 JavaScript。
<link rel="preload" as="script" href="script.js" />
<link rel="preload" as="style" href="style.css" />
<link rel="preload" as="image" href="img.png" />
<link rel="preload" as="video" href="vid.webm" type="video/webm" />
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
4.加快首次渲染
方案
首屏渲染时render树的合成时长之一,DOM树与DOM节点数量息息相关,而如果使用 TextArea存放HTML代码,而浏览器解析的时候不会识别,不被当做DOM去解析,会让首屏渲染DOM树包含的节点大幅减少,从而提高首次渲染速度
在服务端:将一些特殊字符(&之类的)进行HTMLEncode转义,在浏览器端,则需要在首屏区域处于可见状态时,将TextArea的HTML代码取出,将其恢复到DOM树进行渲染,这一行为在 《大型网站性能优化实战》中被称为延迟渲染
大规模内联关键 CSS
你需要一个自动化系统,而不是对每个页面都这样做。仅内联 WordPress 主题主页的 CSS 是有意义的,因为它通常与其他页面是不同的样式表。通常会有一些插件/模块/包、一个关键版本或关键 CSS。这些包(packages)可能适用于你使用的任何 taskrunner 或是如 Grunt、Gulp、Webpack 或框架如 React、Angular、Vue,你还可以找到特定关于 WordPress 或 Drupal 的教程,甚至是手工编码的页面。他们将向页面发送一个无头(headless)浏览器,以确定哪些 CSS 对于不同大小的页面加载上是关键,并为你提供代码或将代码拆分为关键和非关键元素,以便可以适当地加载它们,举几个例子:
Grunt:
https://github.com/filamentgroup/grunt-criticalcss
https://www.npmjs.com/package/grunt-critical-css
https://github.com/bezoerb/grunt-critical
Gulp:
https://github.com/addyosmani/critical
https://www.npmjs.com/package/gulp-critical-css
Webpack:
https://github.com/anthonygore/html-critical-webpack-plugin
https://github.com/GoogleChromeLabs/critters
https://github.com/anthonygore/html-critical-webpack-plugin
https://www.npmjs.com/package/critical-css-webpack-plugin
React:
https://www.npmjs.com/package/react-critical-css
https://github.com/addyosmani/critical-path-css-tools
https://github.com/sergei-zelinsky/react-critical-css
Angular:
https://github.com/addyosmani/critical-path-angular-demo
Vue:
https://github.com/anthonygore/vue-cli-plugin-critical
https://vuejsdevelopers.com/2017/07/24/critical-css-webpack/
Drupal:
https://www.fourkitchens.com/blog/article/use-gulp-automate-your-critical-path-css/
WordPress:
https://joe-watkins.io/javascript/inline-critical-css-with-wordpress/
https://wordpress.org/plugins/wp-criticalcss/
Hand-coded:
https://www.sitelocity.com/critical-path-css-generator
https://jonassebastianohlsson.com/criticalpathcssgenerator/
预加载 Preload
如果不打算内联关键 CSS,那么下一个最佳选择可以是使用预加载 Preload。预加载在加载早期提早获取请求,比平时更快地获取显示页面所需的基本资源。Preload 将预加载资源在浏览器优先级设置为高并异步加载它们,因此它们不会阻塞渲染,也适用于跨域
“新增内容:Chrome 中对于<link rel="preload">
、预取(prefetch)及优先级专业提示:https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf”
5.测试性能工具
YSlow
性能检测插件,以firefox插件的形式
PageSpeed
和YSlow类似的,Google的优化工具PageSpeed
除了有网站的入口,也能以插件的形式在Firefox和Google上运行,也是基于基础性能优化实践规则分析代码,提供一系列页面优化建议
WebPagetest
同样也使用和它们相似的一组最佳性能实践来分析网页,它通过浏览器访问,基于输入的Website URL,以及选择的国家城市、浏览器类型、网络带宽等信息,启动对应的远程服务器上的浏览器进行性能分析测试。(据官网介绍,他们提供的不同城市的性能分析测试,是通过真实的部署在对应城市的物理服务器来实现的,而不是通过其他类似代理或者虚拟机等方式模拟实现的)
window.performance
可以直接在控制台打印 window.performance
,查看诸多的性能指标
- EventCount:从网页打开到现在,执行的事件数量
- memory:堆内存占用大小
- timing:执行各阶段所处的时间戳
- DNS查询耗时= domainLookupEnd - domainLookupStart
- TCP链接耗时= connectEnd - connectStart
- request请求耗时= responseEnd - responseStart
- 解析dom树耗时= domComplete - domInteractive
- 白屏时间= domloadng - fetchStart
- domready时间= domContentLoadedEventEnd - fetchStart
- onload时间= loadEventEnd - fetchStart
chrome-devtool
查看LCP:
在 Chrome 开发者工具中,执行以下步骤:
- Performance > 勾选 “Screenshots”
- 点击 左上方一个“黑色圆点”
- LCP 会显示在时序图上
- 点击节点; 这是就是 LCP 的元素
查看动画帧数,每当你看到 FPS 上方的红色条,这意味着帧率下降如此之低,它可能会损害用户体验。一般来说,绿条越高,FPS 越高。
查看帧数
除此之外,还提供当前帧数FPS的查看 (点击右上角工具栏,找到More tools中的rendering,勾选上FPS meter)
6.性能指标
Core Web Vitals (核心页面指标)是一种速度指标,是 Google 用于衡量用户体验的页面体验信号的一部分
以下是 Core Web Vitals 的三个组成部分及其衡量的内容:
- 最大内容绘制 (LCP) – 视觉负载
- 累积布局偏移 (CLS) – 视觉稳定性
- 首次输入延迟 (FID) – 交互性
性能指标之LCP and FCP
LCP
最大内容绘制 (LCP):https://web.dev/lcp/#what-elements-are-considered
查看方法可以使用chrome的performance
优化LCP
- 越小越快,我们可以选择删除不必要的代码(对于我们自己项目后期维护也比较友好,或者交给打包工具tree shaking下),打包工具的压缩,我们对img的压缩
- CDN分发和缓存的使用
- 资源加载的优先顺序考虑:图片懒加载、css预加载,SSR等
FCP
首次内容绘制 (FCP):https://web.dev/fcp/
我们可以看到FCP实际上就是空白 -> 骨架屏 -> 第一张图/非空白的canvas/svg/文字的出现的时间点,这时候考验的就不是性能就不能怪图片了,主要考验的是js/css加载速度
为了提供良好的用户体验,网站应该努力将首次内容绘制控制在1.8 秒或以内,而我们wapm实验端得到绿色,也就是“令人满意的FCP”应该控制在< 1000(1s)范围内
CLS
CLS 测量元素如何移动或页面布局的稳定性。 它考虑了内容的大小和移动的距离
FID
First Input Delay
可以理解为用户第一次与网页交互,(例如點擊連結、按鈕等等),瀏覽器實際上能夠回應此次互動的時間點
通常,输入延迟(或输入延迟)发生是因为浏览器的主线程忙于执行其他操作,因此它无法(尚未)响应用户。 可能发生这种情况的一个常见原因是浏览器正忙于解析并执行应用程序加载的大型JavaScript文件 / CSS文件。 虽然它正在这样做,它不能运行任何事件监听器,因为它加载的JavaScript可能会告诉它做其他事情。
较长的首次输入延迟通常发生在首次内容绘制 (FCP)和Time to Interactive 可交互时间 (TTI)之间,因为在此期间,页面已经渲染出部分内容,但交互性还尚不可靠。为了说明这种情况的发生缘由,我们在时间轴中加入了 FCP 和 TTI
您可能已经注意到 FCP 和 TTI 之间有相当长的一段时间(包括三段长任务),如果用户在这段时间内尝试与页面进行交互(例如单击一个链接),那么从浏览器接收到单击直至主线程能够响应之前就会有一段延迟。
因为浏览器正在运行任务的过程中,所以浏览器必须等到任务完成后才能对输入作出响应。浏览器必须等待的这段时间就是这位用户在该页面上体验到的 FID 值
FID重要性:首次输入延迟将会是用户对您网站响应度的第一印象,而第一印象对于塑造我们对网站质量和可靠性的整体印象至关重要。
除了最大的内容绘制(LCP) 和累积布局偏移(CLS),首次输入延迟是谷歌在 2021 年夏季推出的页面体验更新中强调的三个核心 Web 要素之一。
由于 FID 是仅通过真实用户交互测量的统计数据,因此无法在实验室环境中复制。
然而,总阻塞时间 (TBT) 是一个衡量浏览器被阻塞时间的指标,因此可以近似地估计 FID。这就是为什么您会在 Semrush 的 Core Web Vitals 报告中看到 TBT 而不是 FID 的原因。
改善FID
減少JavaScript等網頁程式碼的運作時間
編排良好的主線程,能有效決定哪些元素率先載入,而避免過大而慢的代碼阻礙其他元素載入。
JavaScript任务会消耗大量的资源,会影响你的网站速度性能。为了解决这个问题,请删除它们或将长的任务分成小的、异步的任务。至少,一个任务应该在50毫秒以下。(长任务是主线程被阻塞超过 50 毫秒的任何任务。 )
你可以使用setTimeout函数将长任务分成小任务,因为这将使主线程对用户输入的响应没有延迟。
檢查一下有沒有已經失效或不再用的編碼並刪除
減低網頁的請求數和檔案大小
- 適度壓縮圖像
減少主執行緒的工作
減低第三方程式碼的影響
检查长任务
上图是主线程的样子,Chrome 开发工具中 Performance 页签中的那些红色刻度线表示可能存在问题的地方。 通常在主线程上运行太多时间的任务就会被标记,每一个地方都是超出页面负荷的工作并且无法及时响应用户输入。
来源:https://web.dev/long-tasks-devtools
当任务正在运行时,页面无法响应用户输入,这是感受上的延迟。任务越长,用户经历的延迟就越长。任务与任务之间的中断是个机会,页面必须切换到用户输入的任务并响应他们想要的内容。
我们还可以通过chrome的Lighthouse 启动性能审计(但是建议在每次提交代码的时候审计一次不然等到包太大了,工程项目复杂化,很难看出代码哪里性能开始变差)
生成报告后,您可以向下滚动以深入了解 Lighthouse 的建议,这些建议将标记为机会和诊断。
通过诊断出来的 ‘’Avoid long main-thread tasks “ 找到长任务的发生点
7.红宝书最佳实践
避免全局查找
function updateUI() {
let imgs = document.getElementsBtTagName("img");
for(let i = 0; i < imgs.length; i++) {
imgs[i].title = `${document.title} imge ${i}`;
}
let msg = document.getElementById("msg");
msg.innerHTML = "Update complete";
}
这个地方三次引用了全局document对象,而for甚至要引用成百上千次,每次都要遍历作用域链,我们应该保存对document对象的引用,明显提升该函数的性能
function updateUI() {
const doc = document;
let imgs = doc.getElementsBtTagName("img");
for(let i = 0; i < imgs.length; i++) {
imgs[i].title = `${doc.title} imge ${i}`;
}
let msg = doc.getElementById("msg");
msg.innerHTML = "Update complete";
}
不使用with,他主要用途是减少代码量
function updateBody() {
with(document.body) {
console.log(tagName);
}
}
不使用eval
最悲观的情况是,如果出现了eval和with,所有的优化可能是无意义的,因此如果使用它们最简单的做法就是完全不做优化
减少属性查找的迭代次数,属性查找会增加访问对象属性的算法复杂度
下面是错误示范
let query = window.location.href.substring(window.location.href.indexOf("?"))
下面是正确示范
const url = window.location.href;
const query = url.substring(url.indexOf("?"));
比之前省了33%
性能提升其他注意事项
- 原生方法很快,应该尽可能使用原生方法,而不是使用JavaScript写的方法。原生方法是使用C或者C++等编译型语言写的。因此比JavaScript写的方法要快得多。JavaScript经常被忽视的是Math对象上那些执行复杂数学运算的方法。这些方法总是比执行相同任务的JavaScript函数快得多,比如正弦、余弦等
- switch语句很快
- 位操作很快
参考书籍
《大型网站性能优化实战》—周涛明、张荣华、张新兵
《JavaScript高级程序设计》
《你不知道的的JavaScript(上卷)》