성능
성능은 모든 현대 애플리케이션의 필수적인 부분이며 주요 관심사 중 하나입니다.
모니터링
우리는 Grafana 인스턴스 중 하나에서 성능 대시보드를 사용할 수 있습니다. 이 대시보드는 sitespeed.io의 메트릭 데이터를 자동으로 4시간마다 집계합니다. 이 변경 사항은 집계된 페이지 수가 설정된 수에 도달한 후 표시됩니다.
이 페이지들은 sitespeed-measurement-setup
리포지토리 내의 텍스트 파일에 있는 gitlab
에서 찾을 수 있습니다. 모든 프론트엔드 엔지니어는 이 대시보드에 기여할 수 있으며, 텍스트 파일에 페이지의 URL을 추가하거나 제거하는 방식으로 기여할 수 있습니다. 변경 사항은 main
에 병합된 후 다음 예약된 실행에서 실시간으로 반영됩니다.
각 페이지에서 검토해야 할 3개의 추천 고영향 지표(코어 웹 비탈스)가 있습니다:
이 지표의 경우, 숫자가 낮을수록 웹사이트의 성능이 더 좋다는 의미입니다.
사용자 타이밍 API
사용자 타이밍 API는 모든 현대 브라우저에서 사용 가능한 웹 API입니다. 이 API는 코드 내에 특별한 마크를 배치하여 애플리케이션의 사용자 정의 시간 및 기간을 측정할 수 있게 해줍니다. GitLab에서는 Rails, Vue 또는 일반 JavaScript 환경을 포함하여 프레임워크에 관계없이 모든 타이밍을 측정하기 위해 사용자 타이밍 API를 사용할 수 있습니다. 일관성과 편리한 채택을 위해 GitLab은 코드 내에서 사용자 정의 사용자 타이밍 메트릭을 활성화하는 여러 방법을 제공합니다.
사용자 타이밍 API는 mark
와 measure
라는 두 가지 중요한 패러다임을 도입합니다.
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 성능 플러그인을 사용하려면:
-
플러그인을 가져옵니다:
import PerformancePlugin from '~/performance/vue_performance_plugin';
-
Vue 애플리케이션 초기화 전에 사용합니다:
Vue.use(PerformancePlugin, { components: [ 'IdeTreeList', 'FileTree', 'RepoEditor', ] });
플러그인은 성능을 측정할 구성 요소 목록을 받아들입니다. 구성 요소는 name
옵션으로 지정해야 합니다.
필요한 구성 요소에 대해 이 옵션을 명시적으로 설정해야 할 수 있습니다. 코드베이스의 대부분의 구성 요소는 이 옵션이 설정되지 않았습니다:
export default {
name: 'IdeTreeList',
components: {
...
...
}
플러그인은 다음을 캡처하고 저장합니다:
- 구성 요소가 초기화된 경우의 시작 마크 (
beforeCreate()
훅에서) - 구성 요소가 렌더링된 경우의 종료 마크 (다음 애니메이션 프레임의
nextTick
후mounted()
훅에서). 대부분의 경우 이 이벤트는 모든 하위 구성 요소의 부팅을 기다리지 않습니다. 하위 구성 요소를 측정하려면 플러그인 옵션에 포함해야 합니다. - 두 마크 사이의 측정 지속 시간.
저장된 측정값에 접근하기
저장된 측정값에 접근하려면 다음 중 하나를 사용할 수 있습니다:
-
성능 바. 활성화되어 있다면(
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
등이 있습니다. 이는 동일한 페이지에서 다양한 앱에서 제공되는 마크와 측정값을 식별하는 데 도움이 됩니다.
모범 사례
실시간 구성 요소
실시간 기능을 위한 코드를 작성할 때 염두에 두어야 할 몇 가지 사항이 있습니다:
- 서버에 요청을 과부하하지 마십시오.
- 실시간처럼 느껴져야 합니다.
따라서 요청을 보내는 것과 실시간처럼 느끼는 것 사이의 균형을 맞춰야 합니다. 실시간 솔루션을 만들 때 다음 규칙을 사용하십시오.
-
서버는 헤더에서
Poll-Interval
을 통해 얼마나 자주 폴링해야 하는지 알려줍니다.
이를 폴링 간격으로 사용하십시오. 이는 시스템 관리자가 폴링 속도를 변경할 수 있게 해줍니다.
Poll-Interval: -1
은 폴링을 비활성화해야 함을 의미하며, 이는 반드시 구현되어야 합니다. -
HTTP 상태가 2XX와 다른 응답은 폴링을 비활성화해야 합니다.
-
폴링을 위한 공통 라이브러리를 사용하십시오.
-
활성 탭에서만 폴링하십시오. Visibility를 사용하십시오.
-
정기적인 폴링 간격을 사용하고, 백오프 폴링이나 지터는 사용하지 마십시오. 간격은 서버에 의해 제어됩니다.
-
백엔드 코드는 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
를 통해 자동으로 처리되어야 합니다.
애니메이션
opacity
및 transform
속성만 애니메이션 처리하세요. 다른 속성(예: top
, left
, margin
, padding
)은 모두 레이아웃을 재계산하게 하여 훨씬 더 비쌉니다. 이에 대한 자세한 내용은 고성능 애니메이션을 참고하십시오.
레이아웃을 변경해야 하는 경우(예: 사이드바가 주 콘텐츠를 밀어내는 경우) FLIP을 선호하세요. FLIP은 비용이 많이 드는 속성을 한 번 변경한 후 실제 애니메이션을 변환으로 처리할 수 있습니다.
자산 미리 가져오기
API에서 데이터를 미리 가져오는 것 외에도, 우리는 Webpack 구성에서 정의된 이름 있는 JavaScript “청크”의 미리 가져오기를 허용합니다. 청크에 대해 두 가지 유형의 미리 가져오기를 지원합니다:
-
prefetch
링크 유형 은 미래의 탐색을 위해 청크를 미리 가져오는 데 사용됩니다. -
preload
링크 유형 은 현재 탐색에 중요한 청크를 미리 가져오는 데 사용되지만 렌더링 과정에서 나중에 발견됩니다.
prefetch
와 preload
링크 모두 페이지에 로딩 성능 이점을 제공합니다. 두 링크 모두 비동기적으로 가져오며, 기본적으로 제품의 다른 JavaScript 리소스에서 사용되는 자산의 로딩 연기를 방지하는 것과는 달리, prefetch
와 preload
는 어떤 JavaScript 모듈에서도 명시적으로 가져오지 않는 한 가져온 스크립트를 구문 분석하거나 실행하지 않습니다. 이는 나머지 페이지 리소스의 실행을 차단하지 않고 가져온 리소스를 캐시할 수 있게 합니다.
HAML 뷰에서 JavaScript 청크를 미리 가져오기 위해 :prefetch_asset_tags
와 webpack_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 청크를 미리 가져오는 데 대해 as
및 type
속성에 대해 걱정할 필요가 없습니다. 그러나 청크가 현재 탐색에 중요하지 않을 경우, 명시적으로 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.js
및 commons/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.js
가pages/widget/show/index.js
에서만 가져오는 경우, 모듈을pages/widget/show/my_widget.js
에 두고 상대 경로로 가져옵니다 (예:import initMyWidget from './my_widget';
). - 클래스나 모듈이 여러 라우트에서 사용되는 경우, 이를 가져오는 엔트리 포인트에서 가장 가까운 공통 상위 디렉토리에 있는 공유 디렉토리에 배치하세요. 예를 들어,
my_widget.js
가pages/widget/show/index.js
와pages/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를 사용하여 특정 페이지에서만 필요한 라이브러리를 로드하세요.
- 필요한 경우에만 코드를 지연 로드하기 위해 코드 분할 동적 가져기를 사용하세요.
- 고성능 애니메이션
추가 자료
-
WebPage Test 사이트 로딩 시간과 크기를 테스트합니다.
-
Google PageSpeed Insights 웹 페이지를 평가하고 페이지 개선을 위한 피드백을 제공합니다.
-
Browser Diet 웹 페이지 성능을 향상시키기 위한 실용적인 팁을 정리한 커뮤니티 제작 가이드였습니다.