蘇z 发布的文章

最近给笔记本电脑重装了ubuntu server,为能让屏幕自动息屏,在文件/etc/default/grub中添加配置:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash consoleblank=60 i915.enable_psr=0"

随即出现了一个很头疼的问题,即使是无操作有时候屏幕也会突然亮起来,而且诡异的是,屏幕虽然亮了,但是terminal的光标是不闪烁的,看起来像是卡死了一样,这时候虽然按一下键盘,屏幕就会恢复正常。

让AI帮我深度分析后最终测试发现此配置可避免这个问题:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash consoleblank=60 i915.enable_psr=0"

AI说似乎是和我的双显卡配置有关,系统PSR调度出了问题,i915.enable_psr=0关闭PSR才行,另外PSR关闭对系统影响也很有限

git reflog

reflog是引用日志reference logs的简称,它完整的记录了git仓库中HEAD指针的变化历史,比如commit、reset、checkout、switch、merge、pull、rebase、cherry-pick、revert等,也就是说只要HEAD发生变化,就会有一条日志。

比如我从test分支切换到main分支,然后再切换回test分支,输入git reflog会有如下的记录:

41fc2e7 (HEAD -> test, master) HEAD@{0}: checkout: moving from main to test
7876a29 (main) HEAD@{1}: checkout: moving from test to main

从日志中可以看到操作详情,开头的字符串就是HEAD的commit hash。其他情况类似。

示例:

当我们使用git reset --hard 7d8be96回退代码后,又突然后悔了,而且撤回的提交别的分支也没有。这时候git reflog就派上用场了。这时候使用git relog命令输出如下:

7d8be96 (HEAD -> test) HEAD@{0}: reset: moving to 7d8be96e69dea59e2600ba2af3e1671d040281f9
41fc2e7 (master) HEAD@{1}: checkout: moving from main to test
7876a29 (main) HEAD@{2}: checkout: moving from test to main

可以看到首条日志就是我们刚刚的操作reset,而我们想要回退到第二步,即从main刚切换到test的状态,这时只需要使用命令git reset --hard <font style="color:rgb(38, 38, 38);background-color:rgb(250, 250, 250);">41fc2e7</font>就能够无损恢复了。此时reflog如下:

41fc2e7 (HEAD -> test, master) HEAD@{0}: reset: moving to 41fc2e7
7d8be96 HEAD@{1}: reset: moving to 7d8be96e69dea59e2600ba2af3e1671d040281f9
41fc2e7 (HEAD -> test, master) HEAD@{2}: checkout: moving from main to test
7876a29 (main) HEAD@{3}: checkout: moving from test to main

同理只要我们的操作被记录在了reflog中,都可以像乘时光机那样恢复到“过去”

git push --force-with-lease

假如有一个提交A和提交B,在push到仓库之后,突然发现B中有一些问题,于是通过git reset撤销了B这个提交的变更,在修改完之后生成一个新的提交C。这时问题就来了,当我们push的时候git会提示报错,不允许你提交,这时候我们可以用git push --force强制提交。但是会产生另一个问题,如果另一个同事在这个分支上推送了新的提交D,那么在git push --force时,就会把同事的代码给覆盖掉,这是一个非常恐怖的事情。

好在git给我们了解决方法,这种情况下应该使用git push --force-with-lease,它在推送时会检测当前远程分支的指向是否和上次fetch / pull时的指向一致,只有一致的情况下才会允许推送,从而有效避免覆盖别人代码的情况。

git commit --amend --no-edit

当我们完成一些修改commit之后,突然发现还有一些小地方需要修改,修改完之后直接使用git commit --amend --no-edit可以将这个修改和上次提交合并,当然提交的commit hash会变更。

  • --amend: 意思是“修正”。它告诉 git 不要生成一个新的提交(commit),而是把当前的改动“追加”到最近的一次提交上。从 git 的角度看,旧的提交被删除了,取而代之的是一个包含旧内容 + 新内容的新提交。
  • --no-edit: 意思是“不要编辑”。通常使用 --amend 时,git 会打开编辑器让你修改提交信息。加上这个参数后,Git 会直接沿用上一次的提交信息,不再弹出编辑器。

注意:如果修改已经提交到远程分支,那么最好使用git push --force-with-lease命令进行提交。

版本选择符(~、^、@)

写法描述查看方式
HEAD~3当前 commit 的祖父的祖父(沿着父链 3 步)git show HEAD~3
HEAD^3当前 commit 的第 3 个父节点(merge 才有)git show HEAD^3
HEAD@{3}HEAD 三次操作之前的位置(来自 reflog)git show HEAD@{3}

其中的HEAD可以更换为任意的commit hash

git cherry-pick

将提交的变更拷贝一份应用到当前分支上,这相当于是用commit的内容在本分支上新生成了一个提交,因此commit hash也是新的。

参数可以是多个:

  • git cherry-pick A..D;代表挑选从A到D之间的提交(A, D],等价于git cherry-pick B C D
  • git cherry-pick A C F;代表挑选提交A、C、F,他们可以不是连续的

git add 或者 stash 的-p参数

在已有多处改动的前提下,想要只提交某几行的变更,并且保留其他代码,可在git add或者git stash后添加-p参数,此命令将你的改动按照git规则分为多个块(hunk),以交互的方式依次询问你每个hunk是否需要提交,输入y为提交,n为不提交,输入e的话,可以手动控制hunk中每一行是否提交。

git checkout -

可以切换到上一个编辑的分支,多次执行的效果是在两个分支之间来回切换。

从任意提交新建分支

git checkout -b <新分支名> <commit hash>

如上,如果不填写commit hash,以当前HEAD指针新建分支,否则以所填commit hash新建分支。

参考资料:

https://dangitgit.com/zh

chatgpt

1.在github上提issue

通常第三方库都有自己的github仓库地址或者官网,如果有github仓库的话就可以在github上提issue,要注意的是一定要按照仓库作者规定的格式提issue,写明bug复现过程,否则issue可能会被忽略。

这种方法理论上是最好的,但是也有一些缺点:

  • 仓库作者可能不维护了,那么issue将不会被解决
  • 仓库作者不认同观点,可能会拒绝所提的issue
  • 仓库作者接受所提的issue,但认为其优先级低,可能需要很长时间才会解决

2.fork代码自己修改

如果急需解决bug的话,可以尝试自己把原仓库fork下来进行修改,并且将代码发布到npm仓库中,供项目使用。这种方法的缺点有:

  • 增加维护成本,需要和原仓库保持同步,否则会丢失更新
  • 后期可能会和原仓库出现难以合并的分歧
  • 注意合法性问题,可能需要原作者授权

综上,如果原仓库一直在更新的话,需要考虑原仓库和自己仓库的代码同步问题,因此这种方法更适合在原仓库不再维护的情况下使用。

此外还可以给仓库作者提PR,这样就不用担心后续维护的问题了,也是一种比较好的方式,即满足了自己的需求还为开源库做了贡献,但前提是作者接受了PR

3.使用pack-package打补丁

在不修改源代码的情况下,也可以通过直接修改其打包后的代码来实现所需功能,步骤如下:

安装pack-package

npm i patch-package

修改代码

node_modules中找到对应模块的位置,以element-plus为例,假设要对Button组件的点击事件进行修改,找到Button组件以及代码中相应位置,如图所示,添加代码console.log('click')。代码修改后的预期结果是所有用了Button组件的地方,在点击按钮时都会在控制台输出click

验证代码

模版中使用el-button组件:

<template>
  <div>
    <el-button>Default</el-button>
  </div>
</template>

如下图所示代码中并没有主动监听el-button的click事件,但是每次点击时都会在控制台输出"click"

注:如果使用了vite,要注意在重新运行前清除缓存,否则可能会出现代码不生效的问题。

保存修改的代码

为了便于代码的分发,需要把在element-ui中修改的代码保存下来,通过执行命令npx patch-package package-name会生成一个.patch文件,里面包含了修改前后的diff信息。

其中内容为:

diff --git a/node_modules/element-plus/es/components/button/src/use-button.mjs b/node_modules/element-plus/es/components/button/src/use-button.mjs
index 884214f..59c9c6c 100644
--- a/node_modules/element-plus/es/components/button/src/use-button.mjs
+++ b/node_modules/element-plus/es/components/button/src/use-button.mjs
@@ -52,6 +52,7 @@ const useButton = (props, emit) => {
     return false;
   });
   const handleClick = (evt) => {
+    console.log('click')
     if (_disabled.value || props.loading) {
       evt.stopPropagation();
       return;</font>

至此,只要执行patch-package命令即可将patch中的代码并入到node_modules对应文件中。为简化此过程,通常在package.jsonscripts对象中加入"postinstall": "patch-package"配置,这样每当执行npm install之后,都会自动执行patch-package命令。

适用场景

和第二种方法的适用场景类似,但更加适用于修改一些较小的bug,因为node_modules中通常是打包后的代码,特别是经过混淆和压缩的代码,修改难度相对较高。另外打补丁的方法也不需要去另外维护仓库。需要注意的是用这种方法修改,需要锁定包的版本,如果后期需要升级包的话,补丁也需要看实际情况去做相应修改。

4.pnpm patch打补丁

pack-package类似。

使用命令pnpm patch package-name,还是以element plus为例,命令为pnpm patch element-plus,执行命令后会将对应npm包复制到一个叫.pnpm_patches的目录中,如下图所示。

然后就可以在这个新生成的目录中编辑代码,依然在相同位置添加代码console.log('click')。编辑完成后,执行上图中绿色的提交修改的代码。

此时package.json中自动多了代码:

"pnpm": {
  "patchedDependencies": {
    "element-plus": "patches/element-plus.patch"
  }
}

同时目录中也多了patches文件夹,以及element-plus.patch这个补丁文件,里面也是修改前后的diff信息。

最后删除node_modules文件夹,重新安装依赖,查看修改后的代码是否被添加进来。

这种方法的适用场景和使用pack-package是类似的。

5.动态修改代码(Vue)

由于在vue中通过ref可以拿到所使用组件的实例对象,所以理论上我们可以直接修改这个实例对象来达到一些目的。以element-plusel-cascader级联组件为例,查看实例对象:

里面有一个cascaderPanelRef属性,el-cascader是基于cascaderPanel的,它包括了级连组件的核心功能。cascaderPanelRef中有:

注意到里面有一个handleKeyDown的方法,这个方法的详细信息并没有在官方文档中所体现,根据其名称,猜测会对键盘方向键的事件进行处理。尝试对其进行修改:

onMounted(() => {
  let value = refCascade.value
  nextTick(() => {
    let cascaderPanelRef = value.cascaderPanelRef
    let old = cascaderPanelRef.handleKeyDown

    cascaderPanelRef.handleKeyDown = function (...args) {
      old.apply(cascaderPanelRef, args)
      console.log('keydown')
    }
  })
})

如上述代码所示,在保持原有功能的基础上,每当触发keydown事件时,还会在控制台打印keydown

由于handleKeyDowncascaderPanelRef对象自身的方法,并非是从原型链上继承的,因此上面所做的修改只在当前组件实例生效,其它同样的组件是不受任何影响的。

注:vue2中的ref可拿到完整的组件实例对象,而vue3中只能拿到组件主动暴露出来的属性或方法,修改受限,这种方法在vue2中更能发挥作用。

总结

在有条件的情况下,尽可能选择第一种方案,可以免去后续维护代码的工作。否则要修改的代码较多的话可以选择第二种方案,同时也可以给作者提PR,若作者接受的话也可以免去后续代码维护的工作。其他情况酌情选择第3、4、5种方案。

https://www.cnblogs.com/smileZAZ/p/18310045

https://juejin.cn/post/7356534347509497919

1.边框重叠问题

如图,两个input框纵向紧密排列,相邻的边框靠在一起但没有重叠,视觉效果就是边框不对称,中间的边框看起来很粗,很奇怪。

如果直接隐藏上面input的下边框或者下面input的上边框虽然默认状态下是可以解决问题的,但当input框处于:hover状态时,边框会高亮,隐藏边框的地方高亮就会缺失,看起来更奇怪了。为了避免“露馅”,一个很巧妙的方法是通过设置box-shadow来模拟边框的样式。

input {
    border-top: 0;

    &:hover,
    &:focus {
        box-shadow: 0 -1px 0 0 #0364ff;
    }
}

实现效果如下图:

2.vue文件引入外部css文件的作用域问题

vue文件中的script标签以及style标签都可引入外部css文件:

<script setup lang="ts">
    import './css1.scss'
</script>

<style lang="scss" src="./css1.scss"></style>

如上代码,import方式导入的话默认是全局引入的,如果只想组件在内部生效可以使用CSS Modules,使用起来比较麻烦。

style标签默认也是全局引入的,如果只想组件在内部生效可以添加scoped关键字,相对import的方式更加方便。

3.el-table横线问题

使用el-table组件并且开启固定列时,固定列地步可能会出现多一条异常横线的问题,可通过调用表格组件的doLayout方法或者添加以下样式解决:

<style>
  .el-table__fixed-right {
    height: 100% !important;
 }
</style>

4.el-table滚动条位置重置(丢失)问题

如上图的场景,在一个抽屉中会显示多个“子页面”,包括列表页、编辑页、查看页。默认显示列表页,通过v-show控制显示不同页面。列表页中使用了el-table组件显示一个数据列表,当表格中数据较多时,会显示竖向滚动条,此时如果点击显示其他页面然后再切回列表页时,el-table组件会重新渲染,滚动条位置会重置到顶部,原有位置丢失。一个解决方法是,在切换其他页面前获取当前滚动条位置,然后切换回列表页时再恢复滚动条位置:

  // 获取滚动条位置
  public getListScrollHeight() {
    let bodyWrapper = table.querySelector('.el-table__body-wrapper')
    this.saveListScrollHeight = bodyWrapper ? bodyWrapper.scrollTop || 0
  }

  // 恢复滚动条位置
  public recoverListScrollHeight() {
    table.doLayout()

    let bodyWrapper = table.$el.querySelector('.el-table__body-wrapper')
    if (bodyWrapper) {
      this.$nextTick(() => {
        bodyWrapper.scrollTop = this.saveListScrollHeight
      })
    
      table.doLayout()
    }
  }

另外需注意对于表格数据tableData,如果直接赋值新的数组数据或动态添加/删除数据,滚动条位置都不会丢失,但如果先赋空数组[],再赋值新数据,滚动条位置也会丢失。

5.js中的事件传播

js中事件传播分为3个阶段

  • 捕获阶段(Capture Phase):事件从Window对象向下传递到目标元素(触发事件的元素)
  • 目标阶段(Target Phase):事件到达目标元素
  • 冒泡阶段(Bubbling Phase):事件从目标元素开始向上冒泡,直到最外层的DOM元素(Window对象)。

addEventListener方法的第三个参数默认为false,指事件会在冒泡阶段触发,若设置为true,事件将会在捕获阶段触发。因此对于这样一个场景,如需要在全局监听click事件并调用事件处理函数handleClick,如果用默认方式添加事件的话,handleClick会在click事件的冒泡阶段调用,但系统中其它元素也可能会绑定click事件,甚至还可能会用stopPropagation方法阻止事件的冒泡,这会导致全局绑定的handleClick不会被触发。这种场景下可以设置全局添加的click事件在捕获阶段触发,从而避免因调用stopPropagation导致阻止事件冒泡。

6.vue指令相互覆盖问题

有两个vue2项目a, b;a项目被构建为sdk,b项目作为一个业务项目引入了a项目sdk。碰到了这样一个问题:a项目中用了一个指令v-move,并且自己有实现,b项目中有一个同名的指令,在实际应用过程中发现,b项目中的组件触发v-move指令时,有时候触发的是a中的代码,有时候触发的是b中的代码。

造成这个问题的原因主要是两个项目中的指令都是全局注册的,而两个项目共用的是同一个全局Vue,因此在指令注册时可能会互相覆盖。

为避免这个问题可以采取以下方法:

  • 指令改为局部注册
  • 尽可能避免指令重名

7.vue2中this的指向

之前写过一篇博客介绍了关于vue2+vue-property-decorator项目中组件this的指向问题,其中讲到VueComponent组件实例对象是在类实例对象的基础上初始化的,所以针对多个组件实例,VueComponent组件实例对象肯定是不同的,而类实例对象理应是相同的。

但是如上图所示,对于两个组件实例,List类实例对象并不相同,和以前的推断不太一样,推测是vue-property-decorator的缘故,以后有时间再深入研究一下。

web核心性能指标(Core Web Vitals)

Web Vitals是谷歌开发的一系列指标,用于从不同方面去衡量网页的性能。而Core Web Vitals是Web Vitals的一个子集,包含了3个需要特别关注的指标,它们在谷歌搜索算法中扮演着一定的角色,并能够改善网站在谷歌搜索中的权重。

LCP(Largest Contentful Paint,最大内容绘制)

概念

LCP用于用于测量用户感知到的页面加载速度,指从页面开始加载到页面中最大的内容元素被渲染到屏幕上的时间点。这个最大的内容元素可能是图片、视频、SVG、含有背景图的元素、包含文本节点或其它含有文本子元素的块级元素。需要注意的是,随着页面的不断加载,页面中的最大内容元素可能会发生变化,因为更大的内容元素可能还未加载,因此LCP在页面加载过程中也会不断更新直至页面加载完毕。

LCP包含从上一个网页开始的所有卸载时间、连接设置时间、重定向时间和首字节时间(TTFB)。

测量方式

通过PerformanceObserver监测LCP:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

根据LCP的定义,随着网页的加载以上代码会获取到多个startTime值,而所获取到的最后一个值便是页面的LCP值。

通过web-vitals监测LCP:

import {onLCP} from 'web-vitals';

// Measure and log LCP as soon as it's available.
onLCP(console.log);

为获取更精确的LCP,推荐使用第二种方式,详见Web.dev

指标分数

为提供良好的用户体验,网站应努力将Largest Contentful Paint控制在2.5s以内。为确保大多数用户都达到此目标,最好衡量一下网页加载的第 75 个百分位(按移动设备和桌面设备细分)。

INP(Interaction to Next Paint,下一次绘制的交互)

概念

INP衡量页面在整个生命周期中的交互能力(响应能力),通过观察用户在访问网页期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。INP的计算方法是观察用户与网页进行的所有交互并记录延迟时间,最终INP值是观测到的最长交互时间,离群值会被忽略。对于大多数网站来说,延迟时间最长的交互就是INP,但对于包含大量交互的页面来说可能会不准确,因此INP在计算过程中会忽略达到50次交互中的延迟最长的一次交互。

根据INP的定义,只会监测以下类型的交互:

  • 使用鼠标点击。
  • 点按带有触摸屏的设备。
  • 按实体键盘或屏幕键盘上的某个键。

如果页面一直未接收到以上类型的交互,那么页面将不会返回INP值。

2024年3月,INP取代了FID成为Core Web Vitals。

测量方式

通过web-vitals监测INP:

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  console.log(name);    // 'INP'
  console.log(value);   // 512
  console.log(rating);  // 'poor'
});

指标分数

为提供良好的用户体验,网站应努力将Largest Contentful Paint控制在200ms以内,为确保大多数用户都达到此目标,最好衡量一下网页加载实际记录的第 75 个百分位(按移动设备和桌面设备细分):

CLS(Cumulative Layout Shift,累计布局偏移)

概念

布局偏移指页面中的元素所发生的意外移动,这主要发生在当资源以异步方式加载或DOM元素被动态添加到页面的现有内容中时。比如页面上方有一个广告,当用户刚进入页面时还未获取到广告数据,在获取到广告数据后,广告横幅被加载到页面顶部,此时页面中其它的内容都向下移动了广告横幅的高度,这就叫布局偏移。

CLS 用于衡量再页面的整个生命周期中发生的所有意外布局偏移的得分之和。布局的移动可能发生在可见元素从一帧到下一帧改变的任何时候,并且仅当现有元素更改其起始位置时才会发生布局偏移。如果将新元素添加到DOM或现有元素更改了大小,只要没有导致其他可见元素更改其起始位置,就不应将其视为布局偏移。

测量方式

布局偏移计算方式:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Layout shift:', entry);
  }
}).observe({type: 'layout-shift', buffered: true});

每次布局偏移时,都会打印出布局偏移的分数,通过累加每次的分数可获取到累计布局偏移CLS。

累计布局偏移计算也可使用web-vitals。

import {onCLS} from 'web-vitals';

onCLS(console.log);

为获取更精确的CLS,推荐使用第二种方式,详见Web.dev

指标分数

为提供良好的用户体验,网站应努力使CLS得分不超过 0.1。为确保大多数用户都达到此目标,最好衡量一下网页加载的第 75 个百分位(按移动设备和桌面设备细分)。

其他要衡量的重要指标

FCP(First Contentful Paint,首次内容绘制)

概念

FCP用来表示用户感知的页面加载速度,它标记了网页加载时间轴中用户可以看到屏幕上任何内容的一个时间点,衡量了从用户导航到相应网页到该网页的任何内容首次呈现在屏幕上所用的时间,内容包括文本、图片(背景图片)、SVG元素、非白色canvas元素。FCP与LCP的区别是FCP注重第一个出现的内容,而LCP则注重所出现的最大一个内容。

FCP同样包括从上一个网页开始的所有卸载时间、连接设置时间、重定向时间以及首字节时间(TTFB)。

测量方式

通过PerformanceObserver监测FCP:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
    console.log('FCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'paint', buffered: true});

通过web-vitals监测LCP:

import {onFCP} from 'web-vitals';

// Measure and log FCP as soon as it's available.
onFCP(console.log);

为获取更精确的LCP,推荐使用第二种方式,详见Web.dev

指标分数

为提供良好的用户体验,网站应努力使FCP不超过1.8s。为确保您的大多数用户都能达到此目标,建议您衡量第 75 个百分位的网页加载情况(按移动设备和桌面设备细分)。

FID(First Input Delay,首次输入延迟)

概念

当页面还未完全加载完成时,可能因为浏览器主线程还在忙于执行其它操作导致不能及时响应用户的交互。因此使用FID评估用户与页面交互时的响应速度,它衡量了从用户首次与网页交互(如点击链接、按钮等)到浏览器实际能够开始处理事件处理脚本以响应用户交互的时间。FID通常发生在FCP和TTI之间,此时页面已渲染部分内容,但未完全加载完毕,尚不能可靠地互动。

FID本身不会衡量事件处理总时长,也不会衡量浏览器在运行事件处理脚本后更新界面所用的时间,FID衡量的是收到输入事件与主线程下次空闲之间的时间,也就是说即使没有注册与之对应的事件监听器,系统也会衡量 FID。

测量方法

通过PerformanceObserver监测FID:

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    const delay = entry.processingStart - entry.startTime;
    console.log('FID candidate:', delay, entry);
  }
}).observe({type: 'first-input', buffered: true});

以上代码在大多数情况下会输出正确的FID值,但也有特例具体详见Web.dev

通过web-vitals监测LCP:

import {onFID} from 'web-vitals';

// Measure and log FID as soon as it's available.
onFID(console.log);

为获取更精确的LCP,推荐使用第二种方式,详见Web.dev

指标分数

为了提供良好的用户体验,网站应努力将FID控制在100毫秒以内。为确保大多数用户都达到此目标,最好衡量一下网页加载的第75个百分位(按移动设备和桌面设备细分)。

TBT(Total Blocking Time, 总阻塞时间)

概念

TBT指在FCP之后主线程被阻塞并且无法响应用户输入的总时间。

运行的超过50毫秒的任务被称为长任务,主线程在运行长任务时被视为处于“阻塞状态”,如果用户在长任务期间与页面进行交互,浏览器必须等待长任务结束后才能做出响应。

测量方式

由于用户的交互可能会以各种方式影响页面的TBT指标,因此更推荐在实验室场景进行测量,可通过Chrome DevTools中的Performance或者LightHouse测量出TBT。

指标分数

为提供良好的用户体验,网站应努力将TBT控制在200ms以内。

TTI(Time to Interactive,可交互时间)

概念

TTI测量从页面开始加载到其主要子资源加载完成的时间,此时页面能够快速可靠地响应交互。

具体定义:

  1. 从FCP开始。
  2. 向前搜索至少五秒的安静窗口,其中安静窗口定义为:没有长任务并且不超过两个正在进行的网络get请求。
  3. 向后搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在FCP处停止。
  4. TTI 是安静窗口之前最后一个长任务的结束时间(如果未发现长任务,则与FCP相同)。

注意:TTI对异常网络请求和长任务过于敏感,导致该指标高度不稳定。TTI已从LightHose 10中删除。

测量方式

由于用户的交互可能会以各种方式影响页面的TTI指标,因此更推荐在实验室场景进行测量,可通过Chrome DevTools中的Performance或者LightHouse测量出TTI。

指标分数

为提供良好的用户体验,网站应努力将TTI控制在5s以内。

TTFB(Time To First Byte,首字节时间)

概念

TTFB指浏览器发起请求到接收到服务器返回的第一个字节所经过的时间。TTFB包括以下阶段:

  • 重定向时间
  • Server worker启动时间(如果有)
  • DNS查询时间
  • 连接(TCP)和TLS协商时间
  • 发起HTTP请求到接受到服务器响应的一个字节

测量方式

通过PerformanceObserver监测FCP:

new PerformanceObserver((entryList) => {
  const [pageNav] = entryList.getEntriesByType('navigation');

  console.log(`TTFB: ${pageNav.responseStart}`);
}).observe({
  type: 'navigation',
  buffered: true
});

通过web-vitals监测LCP:

import {onTTFB} from 'web-vitals';

// Measure and log TTFB as soon as it's available.
onTTFB(console.log);

指标分数

为提供良好的用户体验,网站应努力将TTFB控制在800ms以内。