성능

성능은 현대 애플리케이션의 필수 요소이자 주요 관심사 중 하나입니다.

모니터링

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

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

각 페이지에서 검토해야 할 3가지 권장 고영향 메트릭(코어 웹 비탈스)이 있습니다:

이러한 메트릭들은 숫자가 작을수록 웹사이트의 성능이 더 좋다는 것을 의미하기 때문에 낮은 숫자가 더 좋습니다.

사용자 타이밍 API

사용자 타이밍 API는 모든 현대 브라우저에서 사용할 수 있는 웹 API입니다. 이 API를 사용하면 응용 프로그램의 코드에 특수 표식을 놓는 것으로 애플리케이션에서 사용자 정의 시간과 기간을 메트릭할 수 있습니다. GitLab에서는 Rails, Vue 또는 기타 JavaScript 환경을 포함하여 모든 프레임워크의 시간을 메트릭할 수 있도록 여러 방법을 제공합니다.

사용자 타이밍 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')
    

특정 메트릭을 조회하려면 표식와 마찬가지로 동일한 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: {
        ...
      ...
    }
    

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

  • 컴포넌트가 초기화된 시점에서의 시작 표식
  • 컴포넌트가 렌더링될 때의 끝 표식(nextTick 후의 다음 애니메이션 프레임). 대부분의 경우 이 이벤트는 모든 하위 컴포넌트가 부트스트랩되기를 기다리지 않습니다. 서브 컴포넌트를 메트릭하려면 이를 플러그인 옵션에 포함해야 합니다.
  • 위의 두 표식 사이의 메트릭 기간.

저장된 메트릭 결과에 접근

저장된 메트릭 결과에 접근하려면 다음 중 하나를 사용할 수 있습니다:

  • 성능 바. 활성화되어 있다면(P + B 키 조합), 개발자 도구 콘솔에서 메트릭 출력을 볼 수 있습니다.
  • “성능” 탭. 성능 프로파일링에 대해 프로파일링이 활성화되어 있다면, 이 탭에서 메트릭을 얻을 수 있습니다 (표식은 제외).
  • 개발자 도구 콘솔. 앞에서 언급한대로 다음과 같이 엔트리를 쿼리할 수 있습니다:

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

네이밍 컨벤션

모든 마크와 메트릭 값은 app/assets/javascripts/performance/constants.js의 상수로 인스턴스화되어야 합니다. 새로운 마크나 메트릭값 레이블을 추가할 준비가 되었다면 해당 패턴을 따를 수 있습니다.

note
이 패턴은 권장사항이며 강제 규칙이 아닙니다.
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로 이동합니다.

  • 이미지를 지연 로딩하도록 HTML에서 src 속성을 data-src로 바꾸고 클래스 lazy를 추가하세요.
  • Rails image_tag 도우미를 사용하는 경우 lazy: false를 제공하지 않는 한 모든 이미지가 기본적으로 지연로드됩니다.

지연 이미지를 포함하는 콘텐츠를 비동기적으로 추가할 때는 gl.lazyLoader.searchLazyImages() 함수를 호출하세요. 이 함수는 지연된 이미지를 찾아 필요한 경우 로드합니다. 일반적으로 이 기능은 지연 로딩 함수의 MutationObserver를 통해 자동 처리되어야 합니다.

애니메이션

불투명도변환 속성만 애니메이션화하세요. 다른 속성(예: top, left, margin, padding 등)을 사용하면 레이아웃을 다시 계산해야 하며, 이는 훨씬 비용이 많이 듭니다. 자세한 내용은 고성능 애니메이션을 참조하세요.

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

자산 미리 불러오기

API에서 데이터를 미리 불러오기뿐만 아니라, 웹팩 구성에 정의된대로 ‘chunks’를 미리 불러올 수 있습니다. 두 가지 유형의 chunks에 대해 미리 불러오기를 지원합니다.

  • prefetch 링크 유형 미래 탐색을 위해 chunk를 미리 불러오기 위해 사용됩니다
  • preload 링크 유형 현재 탐색에 필수적이지만 렌더링 프로세스 후반에 발견되는 chunk를 미리 불러오기 위해 사용됩니다

prefetchpreload 링크는 페이지에 로딩 성능 이점을 제공합니다. 둘 다 비동기적으로 불려오지만, 자바스크립트 자원을 지연시키기에 사용되는 자원들과 달리, prefetchpreload는 자바스크립트 모듈에서 명시적으로 가져오지 않는 이상 가져온 스크립트를 구문 분석하거나 실행하지 않습니다. 이를 통해 가져온 자원을 캐시하고 나머지 페이지 자원의 실행을 차단하지 않을 수 있습니다.

HAML 뷰에서 자바스크립트 chunk를 미리 불러오려면 :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은 chunk를 preload합니다. 불러오기 위해 astype 속성을 걱정할 필요가 없습니다. 그러나 chunk가 현재 탐색에 중요하지 않은 경우 명시적으로 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와 같은 보편적인 라이브러리뿐만 아니라 주요 탐색 및 사이드바를 위한 코드도 포함됩니다. 가능하다면 이러한 번들에서 모듈을 제거하여 코드 크기를 줄이는 것을 목표로 해야 합니다.

특정 페이지 자바스크립트

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

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

중요 사항

  • 엔트리 포인트 간소화 유지: 페이지별 자바스크립트 엔트리 포인트는 가능한 한 간소화되어야 합니다. 이 파일은 유닛 테스트에서 제외되며, 주로 모듈 외부에 있는 클래스 및 메서드를 인스턴스화하고 의존성 주입하는 데 사용해야 합니다. 가져와서 DOM을 읽고 인스턴스화한 다음, 그 이상 아무것도 하지 않아야 합니다.

  • DOMContentLoaded 사용하지 말것: 모든 GitLab 자바스크립트 파일은 defer 속성이 추가됩니다. Mozilla 설명에 따르면 “스크립트는 문서 파싱이 완료된 후 실행되어야 하지만, DOMContentLoaded 이벤트가 발생하기 전에 실행될 것”입니다. 문서가 이미 파싱된 상태이므로 응용 프로그램을 부트스트랩하기 위해 DOMContentLoaded 이벤트가 필요하지 않습니다. 모든 DOM 노드를 이미 사용할 수 있기 때문입니다.

  • 모듈 배치 지원:
    • 클래스 또는 모듈이 특정 경로에만 해당하는 경우 해당 경로에 사용된 엔트리 포인트와 가까이 위치하도록 하세요. 예를 들어, 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보다 우선하여 취해집니다. 중복 코드를 최소화하기 위해 다른 엔트리 포인트에서 하나를 가져올 수 있습니다. 이러한 작업은 자동으로 수행되지 않습니다. 이는 기능을 유연하게 오버라이드하기 위해 자동으로 수행되지 않습니다.

코드 분할

페이지 로드시 즉시 실행되지 않아도 되는 코드(예: 모달, 드롭다운 및 늦게로드할 수 있는 다른 동작)는 동적 import 문으로 비동기적으로 청크(chunk)로 분할되어야 합니다. 이러한 import는 스크립트가 로드된 후에 해결(resolve)되는 프로미스를 반환합니다.

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

동적 import를 생성할 때 webpackChunkName을 사용하면, 그 청크에 대해 결정론적인 파일 이름을 제공하여 GitLab 버전 간에 브라우저에서 캐시됩니다.

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

페이지 크기 최소화

작은 페이지 크기는 특히 모바일 및 연결이 느린 사용자에게 페이지가 더 빨리 로드되도록합니다. 페이지는 브라우저에서 더 빨리 구문 분석되며, 제한된 데이터 요금제를 사용하는 사용자에게는 더 적은 데이터가 사용됩니다.

일반적인 팁:

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

추가 자원