성능

성능은 어떤 현대 애플리케이션에서도 중요한 부분이자 주요 관심사 중 하나입니다.

모니터링

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

이러한 페이지는 sitespeed-measurement-setup 리포지토리 내의 텍스트 파일인 gitlab에 있습니다. 프론트엔드 엔지니어들은 이 대시보드에 기여할 수 있습니다. 페이지의 URL을 텍스트 파일에 추가하거나 제거함으로써 기여할 수 있습니다. 이러한 변경 사항은 main으로 병합된 후 다음 스케줄 실행 시 실시간으로 업데이트됩니다.

각 페이지에서 검토해야 할 권장 고영향 메트릭(핵심 웹 핵심)은 다음과 같습니다:

이러한 메트릭의 경우 더 낮은 숫자가 더 나은 성능을 의미하기 때문에 더 나은 것입니다.

User Timing API

User Timing API는 모든 현대 브라우저에서 사용할 수 있는 웹 API입니다. 이 API를 사용하면 코드 내에서 특별한 표시를 함으로써 응용 프로그램에서 사용자 지정 시간과 기간을 측정할 수 있습니다. GitLab에서는 Rails, Vue 또는 기본 JavaScript 환경을 포함한 프레임워크와 관계없이 모든 시간을 측정할 수 있도록 여러 가지 방법을 제공합니다.

User Timing API에는 두 가지 중요한 패러다임이 소개되었습니다: markmeasure.

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')
    

특정 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 컴포넌트의 성능을 자동으로 캡처하고 측정하며 Vue 라이프사이클 및 User Timing API를 활용합니다.

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() 훅에서)
  • 구성 요소가 렌더링된 시점의 끝 표시 (mounted() 훅에서 nextTick 이후의 다음 애니메이션 프레임에서). 대부분의 경우, 이 이벤트는 모든 하위 구성 요소가 부트스트랩될 때까지 기다리지 않습니다. 하위 구성 요소를 측정하려면 플러그인 옵션에 포함해야 합니다.
  • 위의 두 표시 사이의 측정 시간

저장된 측정값에 액세스

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

  • 성능 표시줄. 활성화되어 있다면 (P + B 키 조합), DevTools 콘솔에서 메트릭 출력을 볼 수 있습니다.
  • DevTools의 “성능” 탭. 성능 프로파일링 시에 이 탭에서 측정값을 얻을 수 있습니다 (표시값은 아님).
  • DevTools 콘솔. 앞에서 언급한 대로, 다음과 같이 항목을 조회할 수 있습니다:

    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

실시간 구성 요소

실시간 기능에 대한 코드를 작성할 때 다음 사항을 염두에 두어야 합니다:

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

따라서 요청을 보내는 횟수와 실시간 느낌 사이의 균형을 맞춰야 합니다. 실시간 솔루션을 만들 때 다음 규칙을 사용합니다.

  1. 서버는 헤더에서 Poll-Interval을 보내어 얼마나 주기적으로 폴링할지 알려줍니다. 폴링 간격으로 사용하십시오. 이를 통해 시스템 관리자가 폴링 속도를 변경할 수 있습니다. Poll-Interval: -1은 폴링을 비활성화해야 한다는 것을 의미하며, 이러한 처리가 필요합니다.
  2. 2XX가 아닌 HTTP 상태로 받은 응답도 폴링을 비활성화해야 합니다.
  3. 폴링에는 공통 라이브러리를 사용합니다.
  4. 활성 탭에서만 폴링합니다. Visibility를 사용합니다.
  5. 백오프 폴링이나 지터를 사용하지 않고 정기적인 폴링 간격을 사용하십시오. 간격은 서버에서 제어됩니다.
  6. 백엔드 코드는 아마도 ETag를 사용할 것입니다. 브라우저가 자동으로 처리하는 304 Not Modified 상태를 확인할 필요가 없고 확인해서는 안 됩니다.

이미지의 지연 로드

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

  • src 속성을 data-src로 이름을 바꾸고 클래스 lazy를 추가하여 이미지를 지연로드하기위한 HTML 이미지를 준비합니다.
  • 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 청크를 사전 로드하려면 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할 것입니다. 자바스크립트 청크를 사전로드하는 데 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">

자산 Footprint 축소

유니버설 코드

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

참고: 페이지에 해당하는 컨트롤러 및 액션이 무엇인지 확실하지 않을 때는 GitLab의 모든 페이지에서 브라우저의 개발자 콘솔에서 document.body.dataset.page를 조사하십시오.

중요한 고려 사항

  • 엔트리 포인트를 가볍게 유지하십시오: 페이지별 JavaScript 엔트리 포인트는 가능한 가볍게 유지해야 합니다. 이러한 파일은 단위 테스트에서 제외되어야 하며, 주로 모듈 외부의 클래스 및 메서드를 인스턴스화 및 의존성 주입하는 데 사용되어야 합니다. 단순히 가져오기, DOM 읽기, 인스턴스화 및 기타 동작을 하시면 됩니다.

  • DOMContentLoaded는 사용하지 말아야 합니다: GitLab의 모든 JavaScript 파일은 defer 속성과 함께 추가됩니다. Mozilla documentation에 따르면, “스크립트는 문서의 구문 분석이 완료된 후에 실행되지만 DOMContentLoaded 이벤트가 발생하기 전에 실행되어야 합니다”. 문서가 이미 구문 분석된 상태이므로 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에서 모두 가져오는 경우, shared 디렉토리에 모듈을 두고 가능한 경우 상대 경로로 가져오십시오 (예: ../shared/my_widget).
  • 엔터프라이즈 에디션 주의 사항: GitLab 엔터프라이즈 에디션의 경우, 페이지별 엔트리 포인트는 같은 이름을 가진 커뮤니티 에디션과 우선순위가 지정됩니다. 즉, ee/app/assets/javascripts/pages/foo/bar/index.js가 존재하는 경우, app/assets/javascripts/pages/foo/bar/index.js보다 우선합니다. 중복 코드를 최소화하려면 다른 엔트리 포인트에서 하나의 엔트리 포인트를 가져올 수 있습니다. 이 작업은 유연성을 위해 자동으로 이루어지지 않으며 기능을 재정의할 수 있게 합니다.

코드 분할

페이지 로드 시 즉시 실행되지 않아도 되는 코드(예: 모달, 드롭다운 및 지연로드할 수 있는 기능과 같은 동작)는 동적 import 문장을 사용하여 비동기적으로 분할되어야 합니다. 이러한 import는 스크립트가 로드된 후에 해결되는 Promise를 반환합니다:

import(/* webpackChunkName: 'emoji' */ '~/emoji')
  .then(/* 무언가를 수행합니다 */)
  .catch(/* 오류를 보고합니다 */)

동적 import를 생성할 때 webpackChunkName을 사용하면, 해당 chunk에 대해 브라우저에서 GitLab 버전 간에 캐시할 수 있는 결정론적인 파일 이름을 제공합니다.

더 많은 정보는 webpack의 코드 분할 문서vue의 동적 컴포넌트 문서에서 확인할 수 있습니다.

페이지 크기 최적화

더 작은 페이지 크기는 특히 모바일 및 연결이 떨어진 환경에서 페이지를 더 빠르게 로드하게 됩니다. 페이지가 브라우저에 의해 더 빨리 구문 분석되며, 데이터 사용량이 제한된 데이터 요금제를 가진 사용자들에게 더 적게 사용됩니다.

일반적인 팁:

  • 새로운 글꼴을 추가하지 마세요.
  • 압축률이 더 높은 글꼴 포맷을 선호하세요. 예를 들어, WOFF2는 WOFF보다 우수하며, WOFF는 TTF보다 우수합니다.
  • 가능한 경우 CSS/JS를 압축하고 최소화하세요(Sprockets 및 webpack에서 이를 수행합니다).
  • 추가 라이브러리를 추가하지 않고도 합리적으로 기능을 구현할 수 있다면 피하세요.
  • 특정 페이지에서만 필요한 라이브러리를 로드하기 위해 위에서 설명한 페이지별 JavaScript를 사용하세요.
  • 초기에 필요하지 않은 코드를 지연로드하기 위해 동적 import를 사용하세요.
  • 고성능 애니메이션

추가 리소스