성능
성능은 현대 애플리케이션의 필수 요소이자 주요 관심사 중 하나입니다.
모니터링
우리는 Grafana 인스턴스 중 하나에서 성능 대시보드를 제공합니다. 이 대시보드는 sitespeed.io에서 매 4시간마다 메트릭 데이터를 자동으로 집계합니다. 이러한 변경 사항은 일정 수의 페이지가 집계된 후에 표시됩니다.
이러한 페이지는 sitespeed-measurement-setup
리포지터리 안의 텍스트 파일인 gitlab
에서 찾을 수 있습니다.
모든 프론트엔드 엔지니어가 이 대시보드에 기여할 수 있습니다. 페이지의 URL을 텍스트 파일에 추가하거나 제거함으로써 기여할 수 있습니다. 변경 사항은 main
브랜치로 Merge된 후 다음 예정된 실행에 실시간으로 반영됩니다.
각 페이지에서 검토해야 할 3가지 권장 고영향 메트릭(코어 웹 비탈스)이 있습니다:
- 가장 큰 콘텐츠 렌더링 시간(Largest Contentful Paint)
- 첫 입력 지연(First Input Delay)
- 누적 레이아웃 이동(Cumulative Layout Shift)
이러한 메트릭들은 숫자가 작을수록 웹사이트의 성능이 더 좋다는 것을 의미하기 때문에 낮은 숫자가 더 좋습니다.
사용자 타이밍 API
사용자 타이밍 API는 모든 현대 브라우저에서 사용할 수 있는 웹 API입니다. 이 API를 사용하면 응용 프로그램의 코드에 특수 표식을 놓는 것으로 애플리케이션에서 사용자 정의 시간과 기간을 메트릭할 수 있습니다. GitLab에서는 Rails, Vue 또는 기타 JavaScript 환경을 포함하여 모든 프레임워크의 시간을 메트릭할 수 있도록 여러 방법을 제공합니다.
사용자 타이밍 API에는 두 가지 중요한 패러다임이 소개됩니다: mark
와 measure
.
Mark는 성능 타임라인상의 타임스탬프입니다. 예를 들어 performance.mark('my-component-start');
는 브라우저가 이 코드를 만난 시간을 기록하게 합니다. 그런 다음, 전역 성능 객체를 다시 조회하여 이 표식에 대한 정보를 얻을 수 있습니다. 예를 들어, 개발자 도구 콘솔에서:
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')
특정 메트릭
을 조회하려면 표식
와 마찬가지로 동일한 API를 사용할 수 있습니다:
performance.getEntriesByName('My component')
모든 캡처된 표식과 메트릭을 조회할 수도 있습니다:
performance.getEntriesByType('mark');
performance.getEntriesByType('measure');
getEntriesByName()
또는 getEntriesByType()
을 사용하면 메트릭의 시작 시간과 기간에 대한 정보가 포함된 PerformanceMeasure 객체의 배열이 반환됩니다.
사용자 타이밍 API 유틸리티
performanceMarkAndMeasure
유틸리티는 특정 환경에 바인딩되어 있지 않기 때문에 GitLab의 어디에서든 사용할 수 있습니다.
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: { ... ... }
플러그인은 다음을 캡처하고 저장합니다:
- 컴포넌트가 초기화된 시점에서의 시작 표식
- 컴포넌트가 렌더링될 때의 끝 표식(
nextTick
후의 다음 애니메이션 프레임). 대부분의 경우 이 이벤트는 모든 하위 컴포넌트가 부트스트랩되기를 기다리지 않습니다. 서브 컴포넌트를 메트릭하려면 이를 플러그인 옵션에 포함해야 합니다. - 위의 두 표식 사이의 메트릭 기간.
저장된 메트릭 결과에 접근
저장된 메트릭 결과에 접근하려면 다음 중 하나를 사용할 수 있습니다:
-
성능 바. 활성화되어 있다면(
P
+B
키 조합), 개발자 도구 콘솔에서 메트릭 출력을 볼 수 있습니다. - “성능” 탭. 성능 프로파일링에 대해 프로파일링이 활성화되어 있다면, 이 탭에서 메트릭을 얻을 수 있습니다 (표식은 제외).
-
개발자 도구 콘솔. 앞에서 언급한대로 다음과 같이 엔트리를 쿼리할 수 있습니다:
performance.getEntriesByType('mark'); performance.getEntriesByType('measure');
네이밍 컨벤션
모든 마크와 메트릭 값은 app/assets/javascripts/performance/constants.js
의 상수로 인스턴스화되어야 합니다. 새로운 마크나 메트릭값 레이블을 추가할 준비가 되었다면 해당 패턴을 따를 수 있습니다.
app-*-start // 시작 '마크'용
app-*-end // 끝 '마크'용
app-* // '메트릭값'용
예를 들어, 'webide-init-editor-start
, mr-diffs-mark-file-tree-end
등이 있습니다. 이를 통해 동일한 페이지에서 다양한 앱에서 나오는 마크와 메트릭값을 식별하는 데 도움이 됩니다.
Best Practices
실시간 컴포넌트
실시간 기능에 대한 코드를 작성할 때 고려해야 할 몇 가지 사항이 있습니다.
- 서버를 과부하로 만들지 마십시오.
- 실시간으로 느껴져야 합니다.
따라서 요청을 보내는 것과 실시간으로 느끼는 것 사이에서 균형을 유지해야 합니다. 실시간 솔루션을 만들 때는 다음 규칙을 사용하세요.
- 서버는 헤더에
Poll-Interval
을 보내어 폴링 간격으로 사용합니다. 이를 폴링 간격으로 사용하세요. 이를 통해 시스템 관리자가 폴링 속도를 변경할 수 있습니다.Poll-Interval: -1
은 폴링을 비활성화해야 하며, 이를 구현해야 합니다. - 2XX가 아닌 HTTP 상태로 응답이 온다면 폴링을 비활성화해야 합니다.
- 폴링을 위해 공통 라이브러리를 사용하세요.
- 활성 탭에서만 폴링을 수행하세요. Visibility를 사용하세요.
- 일반적인 폴링 간격을 사용하세요. 백오프 폴링이나 지터 폴링을 사용하지 마세요. 간격은 서버에서 제어됩니다.
- 백엔드 코드에서는 크게 ETag를 사용합니다. 브라우저가 자동으로 확인하게끔
304 Not Modified
상태를 확인할 필요가 없고 확인해서도 안 됩니다.
이미지의 지연 로드
첫 렌더링 시간을 개선하기 위해 이미지의 지연 로드를 사용하고 있습니다. 이 기능은 실제 이미지 소스를 data-src
속성에 설정하여 작동합니다. HTML이 렌더링되고 JavaScript가 로드된 후에 data-src
의 값이 현재 뷰포트에 이미지가 있는 경우 자동으로 src
로 이동합니다.
- 이미지를 지연 로딩하도록 HTML에서
src
속성을data-src
로 바꾸고 클래스lazy
를 추가하세요. - Rails
image_tag
도우미를 사용하는 경우lazy: false
를 제공하지 않는 한 모든 이미지가 기본적으로 지연로드됩니다.
지연 이미지를 포함하는 콘텐츠를 비동기적으로 추가할 때는
gl.lazyLoader.searchLazyImages()
함수를 호출하세요. 이 함수는 지연된 이미지를 찾아 필요한 경우 로드합니다.
일반적으로 이 기능은 지연 로딩 함수의 MutationObserver
를 통해 자동 처리되어야 합니다.
애니메이션
불투명도
및 변환
속성만 애니메이션화하세요. 다른 속성(예: top
, left
, margin
, padding
등)을 사용하면
레이아웃을 다시 계산해야 하며, 이는 훨씬 비용이 많이 듭니다. 자세한 내용은
고성능 애니메이션을 참조하세요.
레이아웃을 변경해야 하는 경우(예: 사이드바가 주요 콘텐츠를 밀어내는 경우)에는 FLIP을 사용하세요. FLIP을 사용하면 비용이 많이 드는 속성을 한 번만 변경하고 실제 애니메이션을 변환으로 처리할 수 있습니다.
자산 미리 불러오기
API에서 데이터를 미리 불러오기뿐만 아니라, 웹팩 구성에 정의된대로 ‘chunks’를 미리 불러올 수 있습니다. 두 가지 유형의 chunks에 대해 미리 불러오기를 지원합니다.
-
prefetch
링크 유형 미래 탐색을 위해 chunk를 미리 불러오기 위해 사용됩니다 -
preload
링크 유형 현재 탐색에 필수적이지만 렌더링 프로세스 후반에 발견되는 chunk를 미리 불러오기 위해 사용됩니다
prefetch
및 preload
링크는 페이지에 로딩 성능 이점을 제공합니다. 둘 다 비동기적으로 불려오지만,
자바스크립트 자원을 지연시키기에 사용되는 자원들과 달리, prefetch
및
preload
는 자바스크립트 모듈에서 명시적으로 가져오지 않는 이상 가져온 스크립트를 구문 분석하거나 실행하지 않습니다.
이를 통해 가져온 자원을 캐시하고 나머지 페이지 자원의 실행을 차단하지 않을 수 있습니다.
HAML 뷰에서 자바스크립트 chunk를 미리 불러오려면 :prefetch_asset_tags
를 webpack_preload_asset_tag
도우미와 함께 사용하세요.
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
이 스니펫은 결과 HTML 페이지에 새로운 <link rel="preload">
요소를 추가합니다:
<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">
기본적으로 webpack_preload_asset_tag
은 chunk를 preload
합니다. 불러오기 위해 as
및 type
속성을 걱정할 필요가 없습니다.
그러나 chunk가 현재 탐색에 중요하지 않은 경우 명시적으로 prefetch
를 요청해야 합니다.
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
이 스니펫은 결과 HTML 페이지에 새로운 <link rel="prefetch">
요소를 추가합니다:
<link rel="prefetch" href="/assets/webpack/monaco.chunk.js">
자산 크기 축소
범용 코드
main.js
및 commons/index.js
에 포함된 코드는 모든 페이지에서 로드되고
실행됩니다. 필요한 경우가 아니면 이 파일에 아무 것도 추가하지 마십시오. 이러한 번들에는 vue
,
axios
, jQuery
와 같은 보편적인 라이브러리뿐만 아니라 주요 탐색 및 사이드바를 위한 코드도 포함됩니다.
가능하다면 이러한 번들에서 모듈을 제거하여 코드 크기를 줄이는 것을 목표로 해야 합니다.
특정 페이지 자바스크립트
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.config.js
파일에 매뉴얼으로 엔트리 포인트를 추가할 필요가 없습니다.
document.body.dataset.page
를 검사하세요.중요 사항
-
엔트리 포인트 간소화 유지: 페이지별 자바스크립트 엔트리 포인트는 가능한 한 간소화되어야 합니다. 이 파일은 유닛 테스트에서 제외되며, 주로 모듈 외부에 있는 클래스 및 메서드를 인스턴스화하고 의존성 주입하는 데 사용해야 합니다. 가져와서 DOM을 읽고 인스턴스화한 다음, 그 이상 아무것도 하지 않아야 합니다.
-
DOMContentLoaded
사용하지 말것: 모든 GitLab 자바스크립트 파일은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
).
- 클래스 또는 모듈이 특정 경로에만 해당하는 경우 해당 경로에 사용된 엔트리 포인트와 가까이 위치하도록 하세요. 예를 들어,
-
Enterprise Edition 주의사항:
GitLab Enterprise Edition의 경우, 페이지별 엔트리 포인트가 동일한 이름의 Community Edition 버전보다 우선합니다. 따라서
ee/app/assets/javascripts/pages/foo/bar/index.js
가 존재하면app/assets/javascripts/pages/foo/bar/index.js
보다 우선하여 취해집니다. 중복 코드를 최소화하기 위해 다른 엔트리 포인트에서 하나를 가져올 수 있습니다. 이러한 작업은 자동으로 수행되지 않습니다. 이는 기능을 유연하게 오버라이드하기 위해 자동으로 수행되지 않습니다.
코드 분할
페이지 로드시 즉시 실행되지 않아도 되는 코드(예: 모달, 드롭다운 및 늦게로드할 수 있는 다른 동작)는 동적 import 문으로 비동기적으로 청크(chunk)로 분할되어야 합니다. 이러한 import는 스크립트가 로드된 후에 해결(resolve)되는 프로미스를 반환합니다.
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(/* 무언가를 수행 */)
.catch(/* 오류 보고 */)
동적 import를 생성할 때 webpackChunkName
을 사용하면, 그 청크에 대해 결정론적인 파일 이름을 제공하여 GitLab 버전 간에 브라우저에서 캐시됩니다.
더 많은 정보는 webpack의 코드 분할 문서 및 vue의 동적 컴포넌트 문서에서 확인할 수 있습니다.
페이지 크기 최소화
작은 페이지 크기는 특히 모바일 및 연결이 느린 사용자에게 페이지가 더 빨리 로드되도록합니다. 페이지는 브라우저에서 더 빨리 구문 분석되며, 제한된 데이터 요금제를 사용하는 사용자에게는 더 적은 데이터가 사용됩니다.
일반적인 팁:
- 새로운 글꼴을 추가하지 마세요.
- 더 나은 압축 형식을 가진 글꼴 형식을 선호하십시오. 예를 들어, WOFF2는 WOFF보다 좋습니다. TTF보다 더 좋습니다.
- 가능한 곳은 모든 에셋을 압축하고 최소화하십시오 (CSS/JS의 경우, Sprockets 및 webpack이 이를 수행합니다).
- 추가 라이브러리를 추가하지 않고도 합리적으로 기능을 구현할 수 있다면 피하십시오.
- 앞서 설명한 페이지별 JavaScript를 사용하여 특정 페이지에서만 필요한 라이브러리를 로드하세요.
- 처음에 필요하지 않은 코드를 늦게로드하기 위해 동적 import를 사용하세요.
- 높은 성능의 애니메이션
추가 자원
- WebPage Test - 사이트 로딩 시간 및 크기를 테스트하는 데 사용됩니다.
- Google PageSpeed Insights - 웹 페이지 등급 및 페이지 향상을 위한 피드백을 제공합니다.
- Chrome DevTools를 사용한 프로파일링
- Browser Diet - 웹 페이지 성능을 향상시키기 위한 실용적인 팁을 기록한 커뮤니티형 안내서이었습니다.