성능

성능은 모든 최신 애플리케이션에 있어서 필수적이고 주요 관심사 중 하나입니다.

모니터링

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

이러한 페이지는 sitespeed-measurement-setup 리포지터리 내의 텍스트 파일에서 찾을 수 있습니다. 여기에는 gitlab라고 불리는 것이 있습니다. 모든 프론트엔드 엔지니어가 이 대시보드에 기여할 수 있습니다. URL을 텍스트 파일에 추가하거나 제거함으로써 기여할 수 있습니다. 이 변경은 main으로 Merge된 후 다음 예정 실행에서 실시간으로 반영됩니다.

각 페이지에서 검토해야 할 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를 조회하려면, 마크와 마찬가지로 동일한 API를 사용할 수 있습니다:

performance.getEntriesByName('My component')

캡쳐된 모든 표시 및 메트릭을 조회할 수도 있습니다:

performance.getEntriesByType('mark');
performance.getEntriesByType('measure');

getEntriesByName() 또는 getEntriesByType()을 사용하면 PerformanceMeasure objects의 배열이 반환되며 메트릭 시작 시간 및 기간에 대한 정보가 포함됩니다.

사용자 타이밍 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 구성요소의 성능을 자동으로 메트릭하고 메트릭합니다. 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 콘솔. 위에서 언급한 대로, 다음과 같이 조회할 수 있습니다:

    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 등이 있습니다. 이는 동일한 페이지에서 다양한 앱으로부터 발생하는 표시와 메트릭값을 식별하는 데 도움이 됩니다.

모범 사례

실시간 컴포넌트

실시간 기능을 위한 코드를 작성할 때 다음 사항을 고려해야 합니다:

  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로 이동합니다.

  • 레이지 로딩을 위해 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 청크를 미리 가져오기 위해 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">

자산 풋프린트 축소

유니버설 코드

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

note
페이지에 어떤 컨트롤러와 액션이 해당되는지 확실하지 않은 경우, GitLab의 모든 페이지에서 브라우저의 개발자 콘솔에서 document.body.dataset.page를 검사하십시오.

중요 사항

  • 경량 엔트리 포인트 유지: 페이지별 JavaScript 엔트리 포인트는 가능한 가볍게 유지해야 합니다. 이러한 파일은 단위 테스트의 대상이 아니며, 주로 모듈 외부의 클래스 및 메서드를 인스턴스화하고 의존성을 주입하는 데 사용되어야 합니다. 가져오기(import), 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).
  • 기업용 에디션 WARNING: GitLab 기업용 에디션의 경우, 페이지별 엔트리 포인트는 동일한 이름을 가진 커뮤니티 에디션 엔트리 포인트를 무시하고 대체합니다. 따라서 ee/app/assets/javascripts/pages/foo/bar/index.js가 존재하는 경우, 기능을 재정의하기 위해 다른 엔트리 포인트에서 하나의 엔트리 포인트를 가져올 수 있습니다. 이 작업은 기능 재정의의 유연성을 위해 자동으로 이루어지지 않습니다.

코드 분할

페이지 로드 후 즉시 실행되지 않아도 되는 코드(예: 모달, 드롭다운 및 지연로드 가능한 기타 동작)는 동적 가져오기 문으로 비동기적으로 분할되어야 합니다. 이러한 가져오기는 스크립트 로드 후에 해결되는 Promise를 반환합니다.

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

동적 가져오기를 생성할 때 webpackChunkName을 사용하면 브라우저에서 GitLab 버전 간에 캐시될 수 있는 결정적인 파일 이름을 제공합니다.

webpack의 코드 분할 문서vue의 동적 컴포넌트 문서에서 자세한 정보를 찾을 수 있습니다.

페이지 크기 최소화

페이지 크기가 작을수록 특히 모바일 및 느린 인터넷 연결에서 페이지가 더 빨리 로드됩니다. 브라우저가 페이지를 더 빨리 구문 분석하고 데이터 사용량이 제한된 데이터 요금제를 사용하는 사용자의 경우에도 데이터가 덜 사용됩니다.

일반적인 팁:

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

추가 자료