성능

성능은 모든 현대 애플리케이션의 필수적인 부분이며 주요 관심사 중 하나입니다.

모니터링

우리는 Grafana 인스턴스 중 하나에서 성능 대시보드를 사용할 수 있습니다. 이 대시보드는 sitespeed.io의 메트릭 데이터를 자동으로 4시간마다 집계합니다. 이 변경 사항은 집계된 페이지 수가 설정된 수에 도달한 후 표시됩니다.

이 페이지들은 sitespeed-measurement-setup 리포지토리 내의 텍스트 파일에 있는 gitlab에서 찾을 수 있습니다. 모든 프론트엔드 엔지니어는 이 대시보드에 기여할 수 있으며, 텍스트 파일에 페이지의 URL을 추가하거나 제거하는 방식으로 기여할 수 있습니다. 변경 사항은 main에 병합된 후 다음 예약된 실행에서 실시간으로 반영됩니다.

각 페이지에서 검토해야 할 3개의 추천 고영향 지표(코어 웹 비탈스)가 있습니다:

이 지표의 경우, 숫자가 낮을수록 웹사이트의 성능이 더 좋다는 의미입니다.

사용자 타이밍 API

사용자 타이밍 API는 모든 현대 브라우저에서 사용 가능한 웹 API입니다. 이 API는 코드 내에 특별한 마크를 배치하여 애플리케이션의 사용자 정의 시간 및 기간을 측정할 수 있게 해줍니다. GitLab에서는 Rails, Vue 또는 일반 JavaScript 환경을 포함하여 프레임워크에 관계없이 모든 타이밍을 측정하기 위해 사용자 타이밍 API를 사용할 수 있습니다. 일관성과 편리한 채택을 위해 GitLab은 코드 내에서 사용자 정의 사용자 타이밍 메트릭을 활성화하는 여러 방법을 제공합니다.

사용자 타이밍 API는 markmeasure라는 두 가지 중요한 패러다임을 도입합니다.

Mark는 성능 타임라인의 타임스탬프입니다. 예를 들어, performance.mark('my-component-start');는 브라우저가 이 코드가 실행된 시간을 기록하게 합니다. 그런 다음 전역 성능 객체를 다시 쿼리하여 이 마크에 대한 정보를 얻을 수 있습니다. 예를 들어, 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 유틸리티

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 성능 플러그인을 사용하려면:

  1. 플러그인을 가져옵니다:

    import PerformancePlugin from '~/performance/vue_performance_plugin';
    
  2. Vue 애플리케이션 초기화 전에 사용합니다:

    Vue.use(PerformancePlugin, {
      components: [
        'IdeTreeList',
        'FileTree',
        'RepoEditor',
      ]
    });
    

플러그인은 성능을 측정할 구성 요소 목록을 받아들입니다. 구성 요소는 name 옵션으로 지정해야 합니다.

필요한 구성 요소에 대해 이 옵션을 명시적으로 설정해야 할 수 있습니다. 코드베이스의 대부분의 구성 요소는 이 옵션이 설정되지 않았습니다:

export default {
  name: 'IdeTreeList',
  components: {
    ...
  ...
}

플러그인은 다음을 캡처하고 저장합니다:

  • 구성 요소가 초기화된 경우의 시작 마크 (beforeCreate() 훅에서)
  • 구성 요소가 렌더링된 경우의 종료 마크 (다음 애니메이션 프레임의 nextTickmounted() 훅에서). 대부분의 경우 이 이벤트는 모든 하위 구성 요소의 부팅을 기다리지 않습니다. 하위 구성 요소를 측정하려면 플러그인 옵션에 포함해야 합니다.
  • 두 마크 사이의 측정 지속 시간.

저장된 측정값에 접근하기

저장된 측정값에 접근하려면 다음 중 하나를 사용할 수 있습니다:

  • 성능 바. 활성화되어 있다면(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 등이 있습니다. 이는 동일한 페이지에서 다양한 앱에서 제공되는 마크와 측정값을 식별하는 데 도움이 됩니다.

모범 사례

실시간 구성 요소

실시간 기능을 위한 코드를 작성할 때 염두에 두어야 할 몇 가지 사항이 있습니다:

  1. 서버에 요청을 과부하하지 마십시오.
  2. 실시간처럼 느껴져야 합니다.

따라서 요청을 보내는 것과 실시간처럼 느끼는 것 사이의 균형을 맞춰야 합니다. 실시간 솔루션을 만들 때 다음 규칙을 사용하십시오.

  1. 서버는 헤더에서 Poll-Interval을 통해 얼마나 자주 폴링해야 하는지 알려줍니다.
    이를 폴링 간격으로 사용하십시오. 이는 시스템 관리자가 폴링 속도를 변경할 수 있게 해줍니다.
    Poll-Interval: -1은 폴링을 비활성화해야 함을 의미하며, 이는 반드시 구현되어야 합니다.

  2. HTTP 상태가 2XX와 다른 응답은 폴링을 비활성화해야 합니다.

  3. 폴링을 위한 공통 라이브러리를 사용하십시오.

  4. 활성 탭에서만 폴링하십시오. Visibility를 사용하십시오.

  5. 정기적인 폴링 간격을 사용하고, 백오프 폴링이나 지터는 사용하지 마십시오. 간격은 서버에 의해 제어됩니다.

  6. 백엔드 코드는 ETags를 사용할 가능성이 높습니다. 당신은 304 Not Modified의 상태를 확인하지 않아야 하고 확인해서는 안 됩니다. 브라우저가 이를 자동으로 변환해줍니다.

지연 로딩 이미지

첫 렌더링 시간을 개선하기 위해 이미지를 지연 로딩하고 있습니다. 이는 실제 이미지 소스를 data-src 속성에 설정함으로써 작동합니다. HTML이 렌더링되고 JavaScript가 로드된 후, 현재 뷰포트에 이미지가 있을 경우 data-src의 값이 자동으로 src로 이동합니다.

  • HTML에서 지연 로딩을 위해 이미지를 준비하려면 src 속성의 이름을 data-src로 변경하고 lazy 클래스를 추가하십시오.
  • Rails image_tag 헬퍼를 사용하는 경우, lazy: false가 제공되지 않는 한 모든 이미지는 기본적으로 지연 로딩됩니다.

지연 이미지를 포함하는 콘텐츠를 비동기적으로 추가할 때는, gl.lazyLoader.searchLazyImages() 함수를 호출하여 지연 이미지를 검색하고 필요에 따라 로드합니다. 일반적으로 이는 지연 로딩 함수의 MutationObserver를 통해 자동으로 처리되어야 합니다.

애니메이션

opacitytransform 속성만 애니메이션 처리하세요. 다른 속성(예: top, left, margin, padding)은 모두 레이아웃을 재계산하게 하여 훨씬 더 비쌉니다. 이에 대한 자세한 내용은 고성능 애니메이션을 참고하십시오.

레이아웃을 변경해야 하는 경우(예: 사이드바가 주 콘텐츠를 밀어내는 경우) FLIP을 선호하세요. FLIP은 비용이 많이 드는 속성을 한 번 변경한 후 실제 애니메이션을 변환으로 처리할 수 있습니다.

자산 미리 가져오기

API에서 데이터를 미리 가져오는 것 외에도, 우리는 Webpack 구성에서 정의된 이름 있는 JavaScript “청크”의 미리 가져오기를 허용합니다. 청크에 대해 두 가지 유형의 미리 가져오기를 지원합니다:

  • prefetch 링크 유형 은 미래의 탐색을 위해 청크를 미리 가져오는 데 사용됩니다.

  • preload 링크 유형 은 현재 탐색에 중요한 청크를 미리 가져오는 데 사용되지만 렌더링 과정에서 나중에 발견됩니다.

prefetchpreload 링크 모두 페이지에 로딩 성능 이점을 제공합니다. 두 링크 모두 비동기적으로 가져오며, 기본적으로 제품의 다른 JavaScript 리소스에서 사용되는 자산의 로딩 연기를 방지하는 것과는 달리, prefetchpreload는 어떤 JavaScript 모듈에서도 명시적으로 가져오지 않는 한 가져온 스크립트를 구문 분석하거나 실행하지 않습니다. 이는 나머지 페이지 리소스의 실행을 차단하지 않고 가져온 리소스를 캐시할 수 있게 합니다.

HAML 뷰에서 JavaScript 청크를 미리 가져오기 위해 :prefetch_asset_tagswebpack_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는 청크를 preload합니다. JavaScript 청크를 미리 가져오는 데 대해 astype 속성에 대해 걱정할 필요가 없습니다. 그러나 청크가 현재 탐색에 중요하지 않을 경우, 명시적으로 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.jscommons/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를 발생시키기 전에 실행되어야 한다”고 함을 의미합니다. 문서가 이미 구문 분석되었기 때문에, 모든 DOM 노드에 이미 접근할 수 있기 때문에 애플리케이션을 부트스트랩하는 데 DOMContentLoaded는 필요하지 않습니다.

  • 지원 모듈 배치:
    • 클래스나 모듈이 특정 라우트에 특화된 경우, 사용되는 엔트리 포인트에 가까운 곳에 위치하도록 하세요. 예를 들어, my_widget.jspages/widget/show/index.js에서만 가져오는 경우, 모듈을 pages/widget/show/my_widget.js에 두고 상대 경로로 가져옵니다 (예: import initMyWidget from './my_widget';).
    • 클래스나 모듈이 여러 라우트에서 사용되는 경우, 이를 가져오는 엔트리 포인트에서 가장 가까운 공통 상위 디렉토리에 있는 공유 디렉토리에 배치하세요. 예를 들어, my_widget.jspages/widget/show/index.jspages/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(/* 무언가 수행 */)
  .catch(/* 오류 보고 */)

동적 가져기를 생성할 때 webpackChunkName을 사용하세요. 이를 통해 청크의 결정론적인 파일 이름을 제공하며, 이는 GitLab 버전 전체에서 브라우저에 캐시될 수 있습니다.

자세한 내용은 webpack의 코드 분할 문서vue의 동적 컴포넌트 문서를 참조하세요.

페이지 크기 최소화

페이지 크기가 작을수록 페이지 로드 속도가 빨라지며, 특히 모바일 및 느린 연결에서 더욱 그렇습니다. 브라우저가 페이지를 더 빨리 구문 분석하며, 데이터 제한이 있는 사용자에게는 데이터 사용량이 줄어듭니다.

일반적인 팁:

  • 새로운 글꼴을 추가하지 마세요.
  • 압축이 더 잘 되는 글꼴 형식을 선호하세요. 예를 들어, WOFF2는 WOFF보다 낫고, WOFF는 TTF보다 낫습니다.
  • 가능한 경우 자산을 압축하고 최소화하세요 (CSS/JS의 경우 Sprockets와 webpack이 대신 수행합니다).
  • 추가 라이브러리를 추가하지 않고도 합리적으로 기능을 달성할 수 있는 경우, 피하세요.
  • 위에서 설명한 대로 페이지별 JavaScript를 사용하여 특정 페이지에서만 필요한 라이브러리를 로드하세요.
  • 필요한 경우에만 코드를 지연 로드하기 위해 코드 분할 동적 가져기를 사용하세요.
  • 고성능 애니메이션

추가 자료