InfoGrab DocsInfoGrab Docs

성능

요약

성능은 현대 애플리케이션에서 필수적인 요소이자 주요 관심 영역 중 하나입니다. GitLab은 sitespeed.io로 프론트엔드 성능을 지속적으로 측정합니다. 페이지 요약 Grafana 대시보드는 개별 페이지 묶음 전체에 대해 4시간마다 메트릭 데이터를 자동으로 집계합니다.

성능은 현대 애플리케이션에서 필수적인 요소이자 주요 관심 영역 중 하나입니다.

모니터링#

GitLab은 sitespeed.io로 프론트엔드 성능을 지속적으로 측정합니다. 모니터링 수준은 페이지 수준과 여정 수준 두 가지이며 서로를 보완합니다.

페이지 수준 모니터링#

페이지 요약 Grafana 대시보드는 개별 페이지 묶음 전체에 대해 4시간마다 메트릭 데이터를 자동으로 집계합니다.

이 페이지들은 sitespeed-measurement-setup 리포지터리의 gitlab 아래 텍스트 파일에 정의되어 있습니다. 프론트엔드 엔지니어라면 누구나 이 텍스트 파일에 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 성능 플러그인 사용 방법은 다음과 같습니다.

  1. 플러그인을 임포트합니다.

    import PerformancePlugin from '~/performance/vue_performance_plugin';
    
  2. 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의 상수로 인스턴스화해야 합니다. 새 마크나 측정값의 레이블을 추가할 때는 다음 패턴을 따를 수 있습니다.

Note

이 패턴은 권장 사항이며 강제 규칙은 아닙니다.

app-*-start // for a start 'mark'
app-*-end   // for an end 'mark'
app-*       // for 'measure'

예를 들어 webide-init-editor-start, mr-diffs-mark-file-tree-end 등이 있습니다. 같은 페이지에서 서로 다른 앱이 만든 마크와 측정값을 구분하기 위한 규칙입니다.

모범 사례#

실시간 컴포넌트#

실시간 기능 코드를 작성할 때는 다음 두 가지를 염두에 두어야 합니다.

  1. 서버에 요청을 과도하게 보내지 않습니다.
  2. 실시간처럼 느껴져야 합니다.

따라서 요청 전송과 실시간 체감 사이에서 균형을 잡아야 합니다. 실시간 솔루션을 만들 때는 다음 규칙을 따릅니다.

  1. 서버는 헤더에 Poll-Interval을 보내 폴링 간격을 알려 줍니다. 이 값을 폴링 간격으로 사용합니다. 이렇게 하면 시스템 관리자가 폴링 속도를 변경할 수 있습니다. Poll-Interval: -1은 폴링을 비활성화해야 한다는 뜻이며, 반드시 구현해야 합니다.
  2. HTTP 상태가 2XX가 아닌 응답에서도 폴링을 비활성화해야 합니다.
  3. 폴링에는 공통 라이브러리를 사용합니다.
  4. 활성 탭에서만 폴링합니다. Visibility를 사용합니다.
  5. 간격은 서버가 제어하므로 일정한 폴링 간격을 사용하고 백오프 폴링이나 지터는 사용하지 않습니다.
  6. 백엔드 코드는 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에서 데이터를 프리페치하는 것뿐 아니라, 이름이 지정된 JavaScript "청크"도 Webpack 구성에 정의된 대로 프리페치할 수 있습니다. 청크에는 두 가지 프리페치 유형을 지원합니다.

  • 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 파일에 엔트리 포인트를 수동으로 추가할 필요가 전혀 없습니다.

Note

어떤 컨트롤러와 액션이 페이지에 대응하는지 확실하지 않으면, GitLab의 아무 페이지에서 브라우저 개발자 콘솔로 document.body.dataset.page를 확인합니다.

TROUBLESHOOTING: Vite를 사용하는 경우, 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로 로드합니다.
  • 초기에 필요하지 않은 코드는 가능한 모든 곳에서 코드 분할 동적 임포트로 지연 로드합니다.
  • High Performance Animations

추가 자료#

성능

GitLab v19.4
원문 보기

요약

성능은 현대 애플리케이션에서 필수적인 요소이자 주요 관심 영역 중 하나입니다. GitLab은 sitespeed.io로 프론트엔드 성능을 지속적으로 측정합니다. 페이지 요약 Grafana 대시보드는 개별 페이지 묶음 전체에 대해 4시간마다 메트릭 데이터를 자동으로 집계합니다.

성능은 현대 애플리케이션에서 필수적인 요소이자 주요 관심 영역 중 하나입니다.

모니터링#

GitLab은 sitespeed.io로 프론트엔드 성능을 지속적으로 측정합니다. 모니터링 수준은 페이지 수준과 여정 수준 두 가지이며 서로를 보완합니다.

페이지 수준 모니터링#

페이지 요약 Grafana 대시보드는 개별 페이지 묶음 전체에 대해 4시간마다 메트릭 데이터를 자동으로 집계합니다.

이 페이지들은 sitespeed-measurement-setup 리포지터리의 gitlab 아래 텍스트 파일에 정의되어 있습니다. 프론트엔드 엔지니어라면 누구나 이 텍스트 파일에 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 성능 플러그인 사용 방법은 다음과 같습니다.

  1. 플러그인을 임포트합니다.

    import PerformancePlugin from '~/performance/vue_performance_plugin';
    
  2. 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의 상수로 인스턴스화해야 합니다. 새 마크나 측정값의 레이블을 추가할 때는 다음 패턴을 따를 수 있습니다.

Note

이 패턴은 권장 사항이며 강제 규칙은 아닙니다.

app-*-start // for a start 'mark'
app-*-end   // for an end 'mark'
app-*       // for 'measure'

예를 들어 webide-init-editor-start, mr-diffs-mark-file-tree-end 등이 있습니다. 같은 페이지에서 서로 다른 앱이 만든 마크와 측정값을 구분하기 위한 규칙입니다.

모범 사례#

실시간 컴포넌트#

실시간 기능 코드를 작성할 때는 다음 두 가지를 염두에 두어야 합니다.

  1. 서버에 요청을 과도하게 보내지 않습니다.
  2. 실시간처럼 느껴져야 합니다.

따라서 요청 전송과 실시간 체감 사이에서 균형을 잡아야 합니다. 실시간 솔루션을 만들 때는 다음 규칙을 따릅니다.

  1. 서버는 헤더에 Poll-Interval을 보내 폴링 간격을 알려 줍니다. 이 값을 폴링 간격으로 사용합니다. 이렇게 하면 시스템 관리자가 폴링 속도를 변경할 수 있습니다. Poll-Interval: -1은 폴링을 비활성화해야 한다는 뜻이며, 반드시 구현해야 합니다.
  2. HTTP 상태가 2XX가 아닌 응답에서도 폴링을 비활성화해야 합니다.
  3. 폴링에는 공통 라이브러리를 사용합니다.
  4. 활성 탭에서만 폴링합니다. Visibility를 사용합니다.
  5. 간격은 서버가 제어하므로 일정한 폴링 간격을 사용하고 백오프 폴링이나 지터는 사용하지 않습니다.
  6. 백엔드 코드는 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에서 데이터를 프리페치하는 것뿐 아니라, 이름이 지정된 JavaScript "청크"도 Webpack 구성에 정의된 대로 프리페치할 수 있습니다. 청크에는 두 가지 프리페치 유형을 지원합니다.

  • 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 파일에 엔트리 포인트를 수동으로 추가할 필요가 전혀 없습니다.

Note

어떤 컨트롤러와 액션이 페이지에 대응하는지 확실하지 않으면, GitLab의 아무 페이지에서 브라우저 개발자 콘솔로 document.body.dataset.page를 확인합니다.

TROUBLESHOOTING: Vite를 사용하는 경우, 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로 로드합니다.
  • 초기에 필요하지 않은 코드는 가능한 모든 곳에서 코드 분할 동적 임포트로 지연 로드합니다.
  • High Performance Animations

추가 자료#