性能
性能是任何现代应用的重要组成部分,也是主要关注领域之一。
监控
我们在其中一个 Grafana 实例 中提供了性能仪表板。该仪表板每 4 小时自动从 sitespeed.io 聚合指标数据。当聚合了一定数量的页面后,这些变更才会显示。
这些页面可以在 sitespeed-measurement-setup 仓库 中的文本文件里找到,具体在 gitlab 目录下。
任何前端工程师都可以为这个仪表板做出贡献。他们可以通过添加或删除文本文件中的页面 URL 来贡献。变更合并到 main 后,将在下一次计划运行时实时生效。
建议在每个页面上查看以下 3 个高影响力指标(核心网页指标):
对于这些指标,数值越低越好,这意味着网站性能更佳。
用户计时 API
用户计时 API 是一个 Web API 在所有现代浏览器中都可用。它允许您通过在代码中放置特殊标记来测量应用程序中的自定义时间和持续时间。您可以在 GitLab 中使用用户计时 API 来测量任何计时,无论使用什么框架,包括 Rails、Vue 或原生 JavaScript 环境。为了便于一致性和采用,GitLab 提供了多种方式来启用自定义用户计时指标。
用户计时 API 引入了两个重要范式:mark 和 measure。
Mark 是性能时间线上的时间戳。例如,
performance.mark('my-component-start'); 会让浏览器记录遇到此代码的时间。然后,您可以通过再次查询全局 performance 对象来获取有关此标记的信息。例如,在您的 DevTools 控制台中:
performance.getEntriesByName('my-component-start')Measure 是以下两者之间的持续时间:
- 两个标记之间
- 导航开始和一个标记之间
- 导航开始和测量时刻之间
它接受多个参数,其中只有测量名称是必需的。示例:
-
开始和结束标记之间的持续时间:
performance.measure('My component', 'my-component-start', 'my-component-end') -
标记和测量时刻之间的持续时间。在这种情况下省略结束标记。
performance.measure('My component', 'my-component-start') -
导航开始 和实际测量时刻之间的持续时间。
performance.measure('My component') -
导航开始 和标记之间的持续时间。在这种情况下不能省略开始标记,但可以将其设置为
undefined。performance.measure('My component', undefined, 'my-component-end')
要查询特定的 measure,您可以使用与 mark 相同的 API:
performance.getEntriesByName('My component')您还可以查询所有捕获的标记和测量:
performance.getEntriesByType('mark');
performance.getEntriesByType('measure');使用 getEntriesByName() 或 getEntriesByType() 返回一个包含
PerformanceMeasure 对象
的数组,其中包含有关测量开始时间和持续时间的信息。
用户计时 API 工具
您可以在 GitLab 的任何地方使用 performanceMarkAndMeasure 工具,因为它不绑定到任何特定环境。
performanceMarkAndMeasure 接受一个对象作为参数,其中:
| 属性 | 类型 | 必需 | 描述 |
|---|---|---|---|
mark |
String |
否 | 要设置的标记的名称。用于稍后检索标记。如果未指定,则不设置标记。 |
measures |
Array |
否 | 此时进行的测量列表。 |
作为回报,measures 数组中的条目是具有以下 API 的对象:
| 属性 | 类型 | 必需 | 描述 |
|---|---|---|---|
name |
String |
是 | 测量的名称。用于稍后检索标记。每个测量对象都必须指定,否则 JavaScript 会失败。 |
start |
String |
否 | 测量应从哪个标记开始。 |
end |
String |
否 | 测量应到哪个标记结束。 |
示例:
import { performanceMarkAndMeasure } from '~/performance/utils';
...
performanceMarkAndMeasure({
mark: MR_DIFFS_MARK_DIFF_FILES_END,
measures: [
{
name: MR_DIFFS_MEASURE_DIFF_FILES_DONE,
start: MR_DIFFS_MARK_DIFF_FILES_START,
end: MR_DIFFS_MARK_DIFF_FILES_END,
},
],
});Vue 性能插件
该插件利用 Vue 生命周期和用户计时 API 自动捕获和测量指定 Vue 组件的性能。
要使用 Vue 性能插件:
-
导入插件:
import PerformancePlugin from '~/performance/vue_performance_plugin'; -
在初始化 Vue 应用程序之前使用它:
Vue.use(PerformancePlugin, { components: [ 'IdeTreeList', 'FileTree', 'RepoEditor', ] });
该插件接受应测量性能的组件列表。组件应通过其 name 选项指定。
您可能需要在所需的组件上显式设置此选项,因为 代码库中的大多数组件都没有设置此选项:
export default {
name: 'IdeTreeList',
components: {
...
...
}该插件捕获并存储以下内容:
- 组件初始化时的开始 mark(在
beforeCreate()钩子中) - 组件渲染结束时的结束 mark(在
mounted()钩子中nextTick后的下一动画帧)。在大多数情况下,此事件不会等待所有子组件引导完成。要测量子组件,您应该将它们包含在 插件选项中。 - 上述两个标记之间的 Measure 持续时间。
访问存储的测量值
要访问存储的测量值,您可以使用以下任一方法:
-
性能栏。如果您已启用它(
P+B组合键),您可以在 DevTools 控制台中看到指标输出。 -
DevTools 的"性能"选项卡。在分析性能时,您可以在此选项卡中获取测量值(但不是标记)。
-
DevTools 控制台。如上所述,您可以查询条目:
performance.getEntriesByType('mark'); performance.getEntriesByType('measure');
命名约定
所有标记和测量都应使用来自
app/assets/javascripts/performance/constants.js 的常量实例化。当您准备添加新的标记或测量标签时,可以遵循以下模式。
此模式是建议,而不是硬性规定。
app-*-start // 用于开始 'mark'
app-*-end // 用于结束 'mark'
app-* // 用于 'measure'
例如,'webide-init-editor-start、mr-diffs-mark-file-tree-end 等。我们这样做是为了帮助识别来自同一页面上不同应用的标记和测量。
最佳实践
实时组件
在编写实时功能代码时,我们必须牢记以下几点:
- 不要用请求使服务器过载。
- 它应该感觉是实时的。
因此,我们必须在发送请求和实时感觉之间取得平衡。 创建实时解决方案时使用以下规则。
- 服务器通过在标头中发送
Poll-Interval告诉您轮询多少次。 使用它作为您的轮询间隔。这使系统管理员可以更改 轮询速率。Poll-Interval: -1表示您应该禁用轮询,这必须实现。 - HTTP 状态不是 2XX 的响应也应禁用轮询。
- 使用通用库进行轮询。
- 仅在活动选项卡上进行轮询。使用 Visibility。
- 使用常规轮询间隔,不要使用退避轮询或抖动,因为间隔由 服务器控制。
- 后端代码可能正在使用 ETags。您不需要也不应该检查状态
304 Not Modified。浏览器会为您转换它。
图片懒加载
为了改善首次渲染时间,我们对图片使用懒加载。这是通过将实际图片源设置在 data-src 属性上实现的。在 HTML 渲染和 JavaScript 加载后,如果图片在当前视口中,data-src 的值会自动移动到 src。
- 通过将
src属性重命名为data-src并添加类lazy来准备 HTML 中的图片进行懒加载。 - 如果您使用 Rails
image_tag助手,除非提供lazy: false,否则所有图片默认都会懒加载。
当异步添加包含懒图片的内容时,调用函数
gl.lazyLoader.searchLazyImages(),它会搜索懒图片并在需要时加载它们。
通常,它应该通过懒加载函数中的 MutationObserver 自动处理。
动画
只对 opacity 和 transform 属性进行动画处理。其他属性(如 top、left、margin 和 padding)都会导致
布局重新计算,这要昂贵得多。有关详细信息,请参阅
高性能动画。
如果您确实需要更改布局(例如,将主要内容推到一边的侧边栏),请优先使用 FLIP。FLIP 允许您一次性更改昂贵的属性,并使用变换处理实际动画。
资源预加载
除了从 API 预加载数据外,我们还允许预加载 Webpack 配置中定义 的命名 JavaScript “块”。 我们支持两种类型的块预加载:
prefetch链接类型 用于预加载未来导航的块preload链接类型 用于预加载对当前导航至关重要但在 渲染过程后期才发现的块
prefetch 和 preload 链接都为页面带来加载性能优势。两者都是
异步获取的,但与默认用于产品中其他 JavaScript 资源的
延迟加载 相反,prefetch 和 preload 除非在任何 JavaScript 模块中显式导入,否则不会解析或执行获取的脚本。这允许缓存获取的资源而不阻塞
剩余页面资源的执行。
要在 HAML 视图中预加载 JavaScript 块,:prefetch_asset_tags 与
webpack_preload_asset_tag 助手结合使用:
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')此代码片段将添加一个新的 <link rel="preload"> 元素到生成的 HTML 页面中:
<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">默认情况下,webpack_preload_asset_tag 将 preload 该块。您不需要担心
as 和 type 属性用于预加载 JavaScript 块。但是,当块不
关键时,对于当前导航,必须显式请求 prefetch:
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)此代码片段将添加一个新的 <link rel="prefetch"> 元素到生成的 HTML 页面中:
<link rel="prefetch" href="/assets/webpack/monaco.chunk.js">减少资源占用
通用代码
包含在 main.js 和 commons/index.js 中的代码在
所有页面上加载和运行。不要添加任何内容到这些文件中,除非它确实在
所有地方都需要。这些捆绑包包含无处不在的库,如 vue、
axios 和 jQuery,以及主导航和侧边栏的代码。
在可能的情况下,我们应该努力从这些捆绑包中删除模块以减少我们的
代码占用。
页面特定 JavaScript
Webpack 已配置为根据
app/assets/javascripts/pages/* 中的文件结构自动生成入口点捆绑包。
pages 目录中的目录对应于 Rails 控制器和操作。这些
自动生成的捆绑包会自动包含在相应的
页面上。
例如,如果您访问 https://gitlab.com/gitlab-org/gitlab/-/issues,
您将访问 app/controllers/projects/issues_controller.rb
控制器,其中包含 index 操作。如果存在相应的文件
pages/projects/issues/index/index.js,它将被编译成 webpack
捆绑包并包含在页面上。
以前,GitLab 鼓励在 HAML 文件中使用
content_for :page_specific_javascripts,以及
手动生成的 webpack 捆绑包。但是在这个新系统中,您应该
永远不需要手动向 webpack.config.js 文件添加入口点。
当不确定哪个控制器和操作对应于页面时,
从 GitLab 任何页面的浏览器开发者控制台中检查 document.body.dataset.page。
故障排除:
如果使用 Vite,请记住对其支持是新的,您可能会不时遇到意外效果。如果入口点配置正确但 JavaScript 未加载,
请尝试清除 Vite 缓存并重新启动服务:
rm -rf tmp/cache/vite && gdk restart vite
或者,您可以选择使用 Webpack。请遵循这些禁用 Vite 并使用 Webpack的说明。
重要注意事项
-
保持入口点轻量: 页面特定的 JavaScript 入口点应尽可能轻量。这些 文件免除单元测试,应主要用于 实例化和依赖注入存在于入口点脚本外部的类和方法。 只需导入、读取 DOM、实例化,仅此而已。
-
不应使用
DOMContentLoaded: 所有 GitLab JavaScript 文件都使用defer属性添加。 根据 Mozilla 文档, 这意味着"脚本旨在在文档 解析后但在触发DOMContentLoaded之前执行"。因为文档已经 解析,DOMContentLoaded不需要引导应用程序,因为所有 DOM 节点已经在我们手中。 -
支持模块放置:
- 如果一个类或模块_特定于特定路由_,尝试将其放置在
使用的入口点附近。例如,如果
my_widget.js只在pages/widget/show/index.js中导入,您应该 将模块放在pages/widget/show/my_widget.js并使用相对路径导入它 (例如,import initMyWidget from './my_widget';)。 - 如果一个类或模块_被多个路由使用_,将其放在一个
共享目录中,位于导入它的入口点的最接近的公共父目录。
例如,如果
my_widget.js在pages/widget/show/index.js和pages/widget/run/index.js中都导入,那么 将模块放在pages/widget/shared/my_widget.js并尽可能使用相对路径导入 (例如,../shared/my_widget)。
- 如果一个类或模块_特定于特定路由_,尝试将其放置在
使用的入口点附近。例如,如果
-
企业版注意事项: 对于 GitLab 企业版,页面特定入口点会覆盖其 社区版对应入口点(同名),因此如果
ee/app/assets/javascripts/pages/foo/bar/index.js存在,它将优先于app/assets/javascripts/pages/foo/bar/index.js。如果您希望 最小化重复代码,可以从另一个入口点导入一个入口点。 这不是自动完成的,以允许灵活地覆盖 功能。
代码分割
不需要在页面加载时立即运行的代码(例如, 模态框、下拉菜单和其他可以懒加载的行为)应该分割成 使用动态导入语句的异步块。这些 导入返回一个 Promise,在脚本加载后解析:
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(/* do something */)
.catch(/* report error */)生成动态导入时使用 webpackChunkName,因为它为
块提供了确定性文件名,然后可以在 GitLab 版本之间在浏览器中缓存。
更多信息请参阅 webpack 代码分割文档 和 Vue 动态组件文档。
最小化页面大小
更小的页面大小意味着页面加载更快,尤其是在移动设备上 和连接不佳的情况下。页面被浏览器更快地解析, 并且对于有数据上限的用户来说使用的数据更少。
一般提示:
- 不要添加新字体。
- 优先使用压缩更好的字体格式,例如,WOFF2 优于 WOFF,WOFF 优于 TTF。
- 尽可能压缩和最小化资源(对于 CSS/JS,Sprockets 和 webpack 为我们完成)。
- 如果某些功能可以在不添加额外库的情况下合理实现,请避免使用它们。
- 使用上述页面特定 JavaScript 来加载仅在特定页面需要的库。
- 尽可能使用代码分割动态导入来懒加载不需要的代码。
- 高性能动画
额外资源
- WebPage Test 用于测试站点加载时间和大小。
- Google PageSpeed Insights 对网页进行评分并提供反馈以改进页面。
- 使用 Chrome DevTools 进行性能分析
- Browser Diet 是一个社区构建的指南,记录了改进网页性能的实际技巧。