성능

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

모니터링

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

이러한 페이지는 sitespeed-measurement-setup 리포지토리 내의 텍스트 파일에서 찾을 수 있으며 gitlab이라고 불립니다(https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup/-/tree/master/gitlab). 프론트엔드 엔지니어는 이 대시보드에 기여할 수 있습니다. 그들은 페이지의 URL을 텍스트 파일에 추가하거나 제거함으로써 기여할 수 있습니다. 변경 사항은 main으로 병합된 후 다음 예정 실행에서 실시간으로 푸시됩니다.

각 페이지에서 검토할 권장되는 고영향 메트릭(core web vitals)은 다음과 같습니다:

이러한 메트릭에 대해서는 낮은 숫자일수록 웹사이트의 성능이 높다는 것을 의미하기 때문에 더 나은 것입니다.

사용자 타이밍 API

사용자 타이밍 API는 모든 최신 브라우저에서 사용할 수 있는 웹 API입니다(https://caniuse.com/?search=User%20timing). 이 API를 사용하면 코드 내에서 특별한 표시를 함으로써 응용 프로그램에서 사용자 지정 시간과 기간을 측정할 수 있습니다. GitLab에서는 프레임워크에 관계없이 Rails, Vue, 또는 순수 JavaScript 환경을 포함하여 모든 타이밍을 측정할 수 있도록 여러 가지 방법을 제공합니다.

사용자 타이밍 API는 마크(mark)측정(measure)이라는 두 가지 중요한 패러다임을 소개합니다.

마크(mark)는 성능 타임라인 상의 타임스탬프입니다. 예를 들어, performance.mark('my-component-start');는 브라우저가 이 코드를 만나는 시간을 기록합니다. 그런 다음, 다시 글로벌 성능 객체를 쿼리하여 이 마크에 대한 정보를 얻을 수 있습니다. 예를 들어, 개발자 도구 콘솔에서:

performance.getEntriesByName('my-component-start')

측정(measure)은 다음 중 하나 사이의 기간입니다:

  • 두 개의 마크(mark) 사이의 기간
  • 탐색의 시작점과 마크의 사이의 기간
  • 탐색의 시작점에서 측정이 발생한 시점까지의 기간

측정은 해당 측정의 이름이 유일한 필수 인수입니다. 예:

  • 시작점과 끝지점 사이의 기간:

    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 컴포넌트의 성능을 자동으로 캡처하고 측정하며 Vue 라이프사이클과 사용자 타이밍 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: {
    ...
  ...
}

저장된 측정값에 액세스하기

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

  • 성능 표시줄. 활성화되어 있다면 (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를 사용할 것입니다. 브라우저가 대신 변환합니다.

이미지의 지연 로드

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

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

지연 이미지를 포함하는 콘텐츠를 비동기로 추가할 때 gl.lazyLoader.searchLazyImages() 함수를 호출하여 필요에 따라 지연 이미지를 검색하고 로드하세요. 일반적으로 현재 뷰포트의 변경사항은 지연 로딩 함수의 MutationObserver를 통해 자동으로 처리됩니다.

애니메이션

불투명도변환 속성만 애니메이션하세요. 다른 속성(예: 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합니다. 자바스크립트 청크의 미리 불러오기에 대해 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와 같은 파일이 존재한다면 웹팩 번들로 컴파일되어 해당 페이지에 포함됩니다.

이전에 GitLab은 content_for :page_specific_javascripts를 HAML 파일에 사용하여 수동으로 웹팩 번들을 생성하는 것을 장려했었습니다. 그러나 이 새로운 시스템에서는 이제 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 documentation에 따르면 “스크립트는 문서가 구문 분석된 후에 실행되지만 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(/* webpackChunkName: 'emoji' */ '~/emoji')
  .then(/* 어떤 동작 */)
  .catch(/* 오류 보고 */)

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

자세한 내용은 webpack의 코드 분할 문서vue의 동적 컴포넌트 문서에서 확인할 수 있습니다.

페이지 크기 최소화

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

일반적인 팁:

  • 새로운 글꼴을 추가하지 마세요.
  • WOFF2가 WOFF보다 좋으며, TTF보다 압축률이 더 좋습니다.
  • 가능한 경우 자산을 압축하고 최소화하세요(CSS/JS의 경우 Sprockets 및 webpack이 이를 수행합니다).
  • 추가 라이브러리를 추가하지 않고도 합리적으로 기능을 달성할 수 있다면 피하세요.
  • 위에 설명된 대로 페이지별 JavaScript를 사용하여 특정 페이지에서만 필요한 라이브러리를 로드하세요.
  • 초기에 필요하지 않은 코드를 지연로드할 수 있는 동적 가져오기를 사용하세요.

고성능 애니메이션에 대한 정보


추가 자료