성능
GitLab v19.1성능은 현대 애플리케이션에서 필수적인 요소이자 주요 관심 영역 중 하나입니다. GitLab은 sitespeed.io를 사용하여 프론트엔드 성능을 지속적으로 측정합니다. 페이지 요약 Grafana 대시보드는 개별 페이지 세트에 걸쳐 4시간마다 자동으로 메트릭 데이터를 집계합니다.
성능은 현대 애플리케이션에서 필수적인 요소이자 주요 관심 영역 중 하나입니다.
모니터링#
GitLab은 sitespeed.io를 사용하여 프론트엔드 성능을 지속적으로 측정합니다. 모니터링에는 두 가지 보완적인 수준이 있습니다: 페이지 수준과 여정 수준입니다.
페이지 수준 모니터링#
페이지 요약 Grafana 대시보드는 개별 페이지 세트에 걸쳐 4시간마다 자동으로 메트릭 데이터를 집계합니다.
이 페이지들은 gitlab 하위의 sitespeed-measurement-setup 리포지터리 안에 있는 텍스트 파일에 정의되어 있습니다.
어떤 프론트엔드 엔지니어든 해당 텍스트 파일에서 URL을 추가하거나 제거하여 기여할 수 있습니다. 변경 사항은 master에 머지된 후 다음 예약 실행 시 적용됩니다.
각 페이지에서 검토해야 할 영향력 높은 권장 지표(핵심 웹 바이탈)는 3가지입니다:
이 지표들은 수치가 낮을수록 웹사이트의 성능이 더 우수함을 의미합니다.
대시보드는 페이지 로드 중 메인 스레드가 차단되는 시간을 측정하는 Total Blocking Time (TBT)도 표시합니다. TBT는 핵심 웹 바이탈은 아니지만(필드에서 측정할 수 없는 랩 메트릭), 인터랙티비티 문제를 식별하는 데 유용한 진단 지표이며 Sitespeed 대시보드에서 확인할 수 있습니다.
페이지 수준과 여정 수준 대시보드 모두 GitLab 프론트엔드 코드에서 발생하는 User Timing API 마크와 측정값을 캡처합니다. 즉, performanceMarkAndMeasure 유틸리티를 사용하여 코드베이스에 추가한 커스텀 performance.mark() 또는 performance.measure() 호출이 자동으로 수집되어 Grafana에서 확인할 수 있습니다. 이를 활용하면 특정 기능에서 중요한 렌더링 마일스톤을 계측하고 모니터링할 수 있습니다.
여정 수준 모니터링 (사용자 여정)#
페이지 수준 메트릭 외에도, 완전한 사용자 워크플로를 엔드투엔드로 성능 측정합니다. 이를 사용자 여정이라고 하며, 파일 편집 및 커밋, 머지 리퀘스트 생성, 파이프라인 실행과 같이 사용자에게 가장 중요한 워크플로를 나타냅니다.
여정 메트릭은 User Journey Grafana 대시보드에서 시각화됩니다. 각 여정은 단계별 누적 스톱워치 타이밍을 보고하며, Graphite의 sitespeed_io.desktop.gitlab-workflows.<journeyName>.<stopwatchName>에 기록됩니다. Grafana의 diffSeries()를 사용하면 단계별 델타를 계산할 수 있습니다.
여정 스크립트는 sitespeed-measurement-setup 리포지터리의 gitlab/desktop/workflows/에 있습니다. Create_SourceCode_WritingCode 여정(웹 에디터를 통한 파일 편집 및 커밋)은 참조 구현으로 사용됩니다.
자신의 팀을 위한 사용자 여정을 추가하려면 sitespeed-measurement-setup 리포지터리의 사용자 여정 가이드를 따르세요. 가이드에서 다루는 내용:
-
여정 단계와 셀렉터 정의
-
테스트 그룹에서 픽스처 데이터 생성
-
workflow_helper스톱워치로 여정 스크립트 구현 -
Docker로 로컬 테스트
-
Grafana 메트릭 읽기 및 해석
User Timing API#
User Timing API는 모든 현대 브라우저에서 사용 가능한 웹 API입니다. 코드에 특별한 마크를 배치하여 애플리케이션에서 커스텀 시간과 기간을 측정할 수 있습니다. GitLab에서는 Rails, Vue, 일반 JavaScript 환경을 포함하여 프레임워크에 관계없이 어떤 타이밍이든 측정하는 데 User Timing API를 사용할 수 있습니다. 일관성과 채택 편의성을 위해 GitLab은 코드에서 커스텀 사용자 타이밍 메트릭을 활성화하는 여러 방법을 제공합니다.
User Timing 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 객체 배열이 반환됩니다.
User Timing API 유틸리티#
performanceMarkAndMeasure 유틸리티는 특정 환경에 종속되지 않으므로 GitLab 어디에서든 사용할 수 있습니다.
performanceMarkAndMeasure는 객체를 인자로 받으며:
| 속성 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
| mark | String | 아니요 | 설정할 마크의 이름. 나중에 마크를 검색하는 데 사용됩니다. 지정하지 않으면 마크가 설정되지 않습니다. |
| measures | Array | 아니요 | 이 시점에서 수행할 측정값 목록. |
반환 시, measures 배열의 항목은 다음 API를 가진 객체입니다:
| 속성 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
| name | String | 예 | 측정값의 이름. 나중에 마크를 검색하는 데 사용됩니다. 모든 measure 객체에 지정해야 하며, 그렇지 않으면 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 라이프사이클과 User Timing API를 활용하여 지정된 Vue 컴포넌트의 성능을 자동으로 캡처하고 측정합니다.
Vue 성능 플러그인을 사용하려면:
플러그인을 임포트합니다:
import PerformancePlugin from '~/performance/vue_performance_plugin';
Vue 애플리케이션을 초기화하기 전에 사용합니다:
Vue.use(PerformancePlugin, {
components: [
'MyComponent',
'MyOtherComponent',
]
});
플러그인은 성능을 측정할 컴포넌트 목록을 받습니다. 컴포넌트는 name 옵션으로 지정해야 합니다.
코드베이스의 대부분 컴포넌트에는 이 옵션이 설정되어 있지 않으므로, 필요한 컴포넌트에 명시적으로 이 옵션을 설정해야 할 수 있습니다:
export default {
name: 'MyComponent',
components: {
...
...
}
플러그인이 캡처하고 저장하는 항목:
-
컴포넌트가 초기화될 때의 시작 mark (
beforeCreate()훅에서) -
컴포넌트가 렌더링될 때의 종료 mark (
mounted()훅에서nextTick이후 다음 애니메이션 프레임). 대부분의 경우, 이 이벤트는 모든 하위 컴포넌트가 초기화될 때까지 기다리지 않습니다. 하위 컴포넌트를 측정하려면 해당 컴포넌트를 플러그인 옵션에 포함해야 합니다. -
위의 두 마크 사이의 Measure 기간.
저장된 측정값에 접근하기#
저장된 측정값에 접근하려면 다음 중 하나를 사용할 수 있습니다:
Performance bar. 활성화된 경우(P + B 단축키), DevTools 콘솔에서 메트릭 출력을 확인할 수 있습니다.
DevTools의 "Performance" 탭. 성능 프로파일링 시 이 탭에서 측정값(마크는 제외)을 확인할 수 있습니다.
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를 사용하세요.
-
정기적인 폴링 간격을 사용하고, 백오프 폴링이나 지터를 사용하지 마세요. 간격은 서버가 제어합니다.
-
백엔드 코드는 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를 통해 자동으로 처리됩니다.
애니메이션#
opacity와 transform 속성만 애니메이션화하세요. 다른 속성(top, left, margin, padding 등)은 모두 레이아웃 재계산을 유발하며, 이는 훨씬 더 비용이 많이 듭니다. 자세한 내용은
High Performance Animations를 참조하세요.
레이아웃을 변경해야 하는 경우(예: 메인 콘텐츠를 밀어내는 사이드바), FLIP을 사용하는 것을 권장합니다. FLIP을 사용하면 비용이 많이 드는 속성을 한 번만 변경하고 실제 애니메이션은 transform으로 처리할 수 있습니다.
에셋 프리페치#
API에서 데이터를 프리페치하는 것 외에도 Webpack 구성에 정의된 명명된 JavaScript "청크"의 프리페치를 허용합니다. 청크에 대해 두 가지 유형의 프리페치를 지원합니다:
-
prefetch링크 타입은 향후 내비게이션을 위해 청크를 프리페치하는 데 사용됩니다. -
preload링크 타입은 현재 내비게이션에 필수적이지만 렌더링 프로세스 후반까지 발견되지 않는 청크를 프리페치하는 데 사용됩니다.
prefetch와 preload 링크 모두 페이지에 로딩 성능 이점을 가져다 줍니다. 둘 다 비동기적으로 가져오지만, 제품에서 다른 JavaScript 리소스에 기본적으로 사용되는 에셋 로딩 지연과 달리, prefetch와 preload는 어떤 JavaScript 모듈에서도 명시적으로 임포트하지 않으면 가져온 스크립트를 파싱하거나 실행하지 않습니다. 이를 통해 나머지 페이지 리소스의 실행을 차단하지 않고 가져온 리소스를 캐시할 수 있습니다.
HAML 뷰에서 JavaScript 청크를 프리페치하려면 webpack_preload_asset_tag 헬퍼와 결합된 :prefetch_asset_tags가 제공됩니다:
- 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는 청크를 preload합니다. JavaScript 청크를 프리로드할 때 as와 type 속성에 대해 걱정할 필요가 없습니다. 그러나 청크가 현재 내비게이션에 중요하지 않은 경우 명시적으로 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와 같은 어디에서나 사용되는 라이브러리와 메인 내비게이션 및 사이드바 코드가 포함됩니다. 가능하면 코드 공간을 줄이기 위해 이 번들에서 모듈을 제거하는 것을 목표로 해야 합니다.
페이지별 JavaScript#
Webpack은 app/assets/javascripts/pages/*의 파일 구조를 기반으로 자동으로 엔트리 포인트 번들을 생성하도록 구성되어 있습니다. pages 디렉터리의 디렉터리는 Rails 컨트롤러 및 액션에 해당합니다. 이 자동 생성 번들은 해당 페이지에 자동으로 포함됩니다.
예를 들어, https://gitlab.com/gitlab-org/gitlab/-/issues를 방문하면,
index 액션의 app/controllers/projects/issues_controller.rb 컨트롤러에 접근하는 것입니다. pages/projects/issues/index/index.js에 해당 파일이 있으면 webpack 번들로 컴파일되어 페이지에 포함됩니다.
이전에는 GitLab이 HAML 파일에서 content_for :page_specific_javascripts와 수동으로 생성된 webpack 번들의 사용을 권장했습니다. 그러나 이 새로운 시스템에서는 webpack.config.js 파일에 수동으로 엔트리 포인트를 추가할 필요가 없습니다.
페이지에 해당하는 컨트롤러와 액션을 모르는 경우,
GitLab의 어느 페이지에서든 브라우저 개발자 콘솔에서 document.body.dataset.page를 확인하세요.
TROUBLESHOOTING:
Vite를 사용하는 경우, 지원이 새로운 관계로 가끔 예상치 못한 효과가 발생할 수 있습니다. 엔트리포인트가 올바르게 구성되어 있는데도 JavaScript가 로드되지 않는 경우,
Vite 캐시를 지우고 서비스를 재시작해 보세요:
rm -rf tmp/cache/vite && gdk restart vite
또는 Webpack을 대신 사용하도록 선택할 수도 있습니다. Vite 비활성화 및 Webpack 사용 지침을 따르세요.
중요 고려 사항#
-
엔트리 포인트를 가볍게 유지하세요: 페이지별 JavaScript 엔트리 포인트는 최대한 가볍게 유지해야 합니다. 이 파일은 단위 테스트에서 제외되며, 엔트리 포인트 스크립트 외부의 모듈에 있는 클래스와 메서드의 인스턴스화 및 의존성 주입에 주로 사용해야 합니다. 임포트, DOM 읽기, 인스턴스화만 수행하고 그 이상은 하지 마세요.
-
DOMContentLoaded는 사용하지 마세요: 모든 GitLab JavaScript 파일은defer속성으로 추가됩니다. Mozilla 문서에 따르면, 이는 "스크립트가 문서가 파싱된 후,DOMContentLoaded발생 전에 실행되도록 의도되었음"을 의미합니다. 문서가 이미 파싱되었으므로, 모든 DOM 노드가 이미 사용 가능하기 때문에DOMContentLoaded는 애플리케이션을 부트스트랩하는 데 필요하지 않습니다. -
지원 모듈 위치 지정:
클래스나 모듈이 특정 라우트에 특화되어 있다면, 사용되는 엔트리 포인트 가까이에 배치하세요. 예를 들어,
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보다 우선합니다. 중복 코드를 최소화하려면 하나의 엔트리 포인트에서 다른 것을 임포트할 수 있습니다. 기능 재정의에 유연성을 허용하기 위해 이것은 자동으로 수행되지 않습니다.
코드 분할#
페이지 로드 시 즉시 실행할 필요가 없는 코드(예: 모달, 드롭다운, 지연 로드할 수 있는 기타 동작)는 동적 임포트 구문으로 비동기 청크로 분할해야 합니다. 이 임포트는 스크립트가 로드된 후 이행되는 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 — 웹 페이지를 평가하고 페이지 개선을 위한 피드백을 제공합니다.
-
Browser Diet — 웹 페이지 성능 개선을 위한 실용적인 팁을 모아놓은 커뮤니티 가이드입니다.