성능
성능은 어떤 현대 애플리케이션에서도 중요한 부분이자 주요 관심사 중 하나입니다.
모니터링
우리의 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에는 두 가지 중요한 패러다임이 소개되었습니다: mark
와 measure
.
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 성능 플러그인을 사용하려면:
-
플러그인을 가져옵니다:
import PerformancePlugin from '~/performance/vue_performance_plugin';
-
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
실시간 구성 요소
실시간 기능에 대한 코드를 작성할 때 다음 사항을 염두에 두어야 합니다:
- 서버에 요청을 과도하게 보내지 않습니다.
- 실시간으로 느껴져야 합니다.
따라서 요청을 보내는 횟수와 실시간 느낌 사이의 균형을 맞춰야 합니다. 실시간 솔루션을 만들 때 다음 규칙을 사용합니다.
- 서버는 헤더에서
Poll-Interval
을 보내어 얼마나 주기적으로 폴링할지 알려줍니다. 폴링 간격으로 사용하십시오. 이를 통해 시스템 관리자가 폴링 속도를 변경할 수 있습니다.Poll-Interval: -1
은 폴링을 비활성화해야 한다는 것을 의미하며, 이러한 처리가 필요합니다. - 2XX가 아닌 HTTP 상태로 받은 응답도 폴링을 비활성화해야 합니다.
- 폴링에는 공통 라이브러리를 사용합니다.
- 활성 탭에서만 폴링합니다. Visibility를 사용합니다.
- 백오프 폴링이나 지터를 사용하지 않고 정기적인 폴링 간격을 사용하십시오. 간격은 서버에서 제어됩니다.
- 백엔드 코드는 아마도 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
에서 자동으로 처리되어야 합니다.
애니메이션
opacity
및 transform
속성만 애니메이션화합니다. 다른 속성(예: top
, left
, margin
, padding
)은 모두 레이아웃을 다시 계산하게되어 훨씬 비용이 많이 듭니다. 이에 대한 자세한 내용은 고성능 애니메이션을 참조하십시오.
레이아웃을 변경해야 하는 경우(예: 사이드바가 주요 콘텐츠를 밀어내는 경우) FLIP을 선호하십니다. FLIP을 사용하면 비용이 많이 드는 속성을 한 번에 변경하고 실제 애니메이션을 변형으로 다룰 수 있습니다.
자산 사전 로드
API에서 데이터를 사전 로드할 수 있는 것 외에도 Webpack 구성에 정의된 이름이 지정된 JavaScript “청크”를 사전 로드할 수 있습니다. 우리는 청크에 대해 두 가지 유형의 사전 로드를 지원합니다:
-
prefetch
링크 타입 는 미래 탐색을 위해 청크를 사전 로드하는 데 사용됩니다. -
preload
링크 타입 는 현재 탐색에는 중요하지만 나중에 렌더링 프로세스에서 발견되는 청크를 사전 로드하는 데 사용됩니다.
prefetch
와 preload
링크는 모두 페이지로 로딩 성능 이점을 제공합니다.
두 가지 모두 비동기로 가져오지만 미리 불러오는 자바스크립트 리소스를
기본적으로 제품의 다른 JavaScript 리소스에 사용되는 로딩을 뒤로 미루는 것과는 달리,
prefetch
및 preload
는 가져온 스크립트를 명시적으로 임포트하는 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
할 것입니다.
자바스크립트 청크를 사전로드하는 데 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">
자산 Footprint 축소
유니버설 코드
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.config.js
파일에 수동으로 엔트리 포인트를 추가할 필요가 없습니다.
참고:
페이지에 해당하는 컨트롤러 및 액션이 무엇인지 확실하지 않을 때는 GitLab의 모든 페이지에서 브라우저의 개발자 콘솔에서 document.body.dataset.page
를 조사하십시오.
중요한 고려 사항
-
엔트리 포인트를 가볍게 유지하십시오: 페이지별 JavaScript 엔트리 포인트는 가능한 가볍게 유지해야 합니다. 이러한 파일은 단위 테스트에서 제외되어야 하며, 주로 모듈 외부의 클래스 및 메서드를 인스턴스화 및 의존성 주입하는 데 사용되어야 합니다. 단순히 가져오기, DOM 읽기, 인스턴스화 및 기타 동작을 하시면 됩니다.
-
DOMContentLoaded
는 사용하지 말아야 합니다: GitLab의 모든 JavaScript 파일은defer
속성과 함께 추가됩니다. Mozilla documentation에 따르면, “스크립트는 문서의 구문 분석이 완료된 후에 실행되지만DOMContentLoaded
이벤트가 발생하기 전에 실행되어야 합니다”. 문서가 이미 구문 분석된 상태이므로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
에서 모두 가져오는 경우,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를 사용하세요.
- 고성능 애니메이션
추가 리소스
- WebPage Test: 사이트 로드 시간 및 크기를 테스트하는 데 사용됩니다.
- Google PageSpeed Insights: 웹 페이지 등급을 매기고 페이지를 개선하기 위한 피드백을 제공합니다.
- Chrome DevTools를 사용한 프로파일링
- Browser Diet: 웹 페이지 성능을 개선하기 위한 실용적인 팁을 카탈로그화한 커뮤니티 제작 가이드입니다.