성능
성능은 모든 현대 애플리케이션의 필수 요소이자 주요 관심사 중 하나입니다.
모니터링
우리는 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)은 다음과 같습니다:
- 가장 큰 콘텐츠 페인트(Largest Contentful Paint)
- 첫 입력 지연(First Input Delay)
- 누적 레이아웃 이동(Cumulative Layout Shift)
이러한 메트릭에 대해서는 낮은 숫자일수록 웹사이트의 성능이 높다는 것을 의미하기 때문에 더 나은 것입니다.
사용자 타이밍 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 성능 플러그인을 사용하려면:
-
플러그인을 가져오세요:
import PerformancePlugin from '~/performance/vue_performance_plugin';
-
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
실시간 구성 요소
실시간 기능에 대한 코드를 작성할 때 고려해야 할 사항은 다음과 같습니다:
- 서버에 요청을 과도하게 보내지 마십시오.
- 실시간으로 느껴져야 합니다.
따라서 요청을 보내는 것과 실시간 느낌 사이의 균형을 유지해야 합니다. 실시간 솔루션을 만들 때 다음 규칙을 사용하세요.
- 서버는 헤더에서
Poll-Interval
을 보내어 얼마나 주기적으로 폴링해야 하는지 알려줍니다. 이를 폴링 간격으로 사용하세요. 시스템 관리자가 폴링 속도를 변경할 수 있게 합니다.Poll-Interval: -1
은 폴링을 비활성화해야 하며, 이는 구현해야 합니다. - 2XX 이외의 HTTP 상태로 받은 응답은 폴링을 비활성화해야 합니다.
- 폴링에 공통 라이브러리를 사용하세요.
- 활성 탭에서만 폴링하세요. Visibility를 사용하세요.
- 규칙적인 폴링 간격을 사용하세요. 폴백 폴링이나 흔들림 폴링을 사용하지 마세요. 간격은 서버에서 제어됩니다.
- 백엔드 코드는 가능성이 있는 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
링크 타입- 현재 탐색에 중요한 청크를 미리 불러오지만 렌더링 프로세스 중에 나준 후에 발견되는 경우에 사용됩니다
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
합니다. 자바스크립트 청크의 미리 불러오기에 대해 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
와 같은 파일이 존재한다면 웹팩 번들로 컴파일되어 해당 페이지에 포함됩니다.
이전에 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.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
보다 우선합니다. 중복 코드를 최소화하려면 한 엔트리 포인트를 다른 엔트리 포인트에서 가져올 수 있습니다. 이는 기능을 덮어쓰기 위해 유연성을 제공하기 위해 자동으로 이루어지지 않습니다.
코드 분할
페이지 로드 후 즉시 실행되지 않아도 되는 코드(예: 모달, 드롭다운 등의 지연 로드 가능한 동작)는 동적 가져오기 문으로 비동기적으로 분할되어야 합니다. 이러한 가져오기는 스크립트가 로드된 후에 해결되는 약속을 반환합니다.
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(/* 어떤 동작 */)
.catch(/* 오류 보고 */)
동적 가져오기를 생성할 때 webpackChunkName
을 사용하세요.
이는 브라우저에서 GitLab 버전 간에 캐시할 수 있는 결정론적인 파일 이름을 제공합니다.
자세한 내용은 webpack의 코드 분할 문서 및 vue의 동적 컴포넌트 문서에서 확인할 수 있습니다.
페이지 크기 최소화
더 작은 페이지 크기는 특히 모바일이나 느린 연결에서 페이지가 더 빠르게 로드됩니다. 페이지는 브라우저에서 더 빨리 구문 분석되며, 제한된 데이터 요금제를 사용하는 사용자들은 적은 양의 데이터를 사용하게 됩니다.
일반적인 팁:
- 새로운 글꼴을 추가하지 마세요.
- WOFF2가 WOFF보다 좋으며, TTF보다 압축률이 더 좋습니다.
- 가능한 경우 자산을 압축하고 최소화하세요(CSS/JS의 경우 Sprockets 및 webpack이 이를 수행합니다).
- 추가 라이브러리를 추가하지 않고도 합리적으로 기능을 달성할 수 있다면 피하세요.
- 위에 설명된 대로 페이지별 JavaScript를 사용하여 특정 페이지에서만 필요한 라이브러리를 로드하세요.
- 초기에 필요하지 않은 코드를 지연로드할 수 있는 동적 가져오기를 사용하세요.
고성능 애니메이션에 대한 정보
추가 자료
- 사이트 로딩 시간 및 크기를 테스트하기 위한 WebPage Test
- 웹 페이지를 평가하고 페이지를 개선하는 데 피드백을 제공하는 Google PageSpeed Insights
- Chrome DevTools를 사용한 프로파일링 Chrome DevTools를 사용한 프로파일링
- 웹 페이지 성능을 향상시키는 실용적인 팁을 기록한 Browser Diet는 커뮤니티가 작성한 가이드입니다.