GitLab CI와 GitLab 아티팩트의 Hoodoo로 성능을 가시화하는 방법
게시 됨: 2022-03-10성능 저하는 우리가 매일 직면하는 문제입니다. 우리는 애플리케이션이 빠르게 타오르도록 노력할 수 있지만 곧 우리가 시작한 곳에서 끝납니다. 이것은 새로운 기능이 추가되고 우리가 지속적으로 추가 및 업데이트하는 패키지에 대해 다시 생각하지 않거나 코드의 복잡성에 대해 생각하지 않는다는 사실 때문에 발생합니다. 일반적으로 작은 일이지만 여전히 작은 일에 관한 모든 것입니다.
우리는 느린 앱을 가질 여유가 없습니다. 성능은 고객을 확보하고 유지할 수 있는 경쟁 우위입니다. 앱을 다시 최적화하는 데 정기적으로 시간을 할애할 여유가 없습니다. 비용이 많이 들고 복잡합니다. 이는 비즈니스 관점에서 볼 때 성능의 모든 이점에도 불구하고 수익성이 거의 없음을 의미합니다. 어떤 문제에 대한 해결책을 찾는 첫 번째 단계로 문제를 가시화해야 합니다. 이 기사는 정확히 당신을 도울 것입니다.
참고 : Node.js에 대한 기본적인 이해가 있고 CI/CD의 작동 방식에 대한 막연한 아이디어가 있고 앱의 성능이나 앱이 가져올 수 있는 비즈니스 이점에 관심이 있다면 계속 진행해도 좋습니다.
프로젝트의 성과 예산을 만드는 방법
우리가 스스로에게 물어야 할 첫 번째 질문은 다음과 같습니다.
"퍼포먼스 프로젝트란?"
"어떤 측정항목을 사용해야 하나요?"
"이 메트릭의 어떤 값이 허용됩니까?"
메트릭 선택은 이 기사의 범위를 벗어나고 프로젝트 컨텍스트에 크게 의존하지만 Philip Walton의 사용자 중심 성능 메트릭을 읽는 것으로 시작하는 것이 좋습니다.
내 관점에서 보면 라이브러리 크기(KB)를 npm 패키지의 메트릭으로 사용하는 것이 좋습니다. 왜요? 다른 사람들이 자신의 프로젝트에 귀하의 코드를 포함하는 경우 애플리케이션의 최종 크기에 대한 코드의 영향을 최소화하기를 원할 수 있기 때문입니다.
사이트의 경우 TTFB(Time To First Byte)를 메트릭으로 간주합니다. 이 메트릭은 서버가 무언가에 응답하는 데 걸리는 시간을 보여줍니다. 이 메트릭은 중요하지만 서버 렌더링 시간부터 시작하여 대기 시간 문제로 끝나는 모든 것을 포함할 수 있기 때문에 상당히 모호합니다. 따라서 정확히 무엇으로 구성되어 있는지 알아보기 위해 Server Timing 또는 OpenTracing과 함께 사용하는 것이 좋습니다.
TTI(Time to Interactive) 및 첫 번째 의미 있는 페인트(후자는 곧 LCP(Large Contentful Paint)로 대체됨)와 같은 메트릭도 고려해야 합니다. 인지된 성능의 관점에서 이 두 가지가 가장 중요하다고 생각합니다.
그러나 측정항목은 항상 컨텍스트와 관련 이 있으므로 이를 당연하게 받아들이지 마십시오. 당신의 특정한 경우에 무엇이 중요한지 생각해 보십시오.
측정항목에 대한 원하는 값을 정의하는 가장 쉬운 방법은 경쟁업체 또는 자신을 사용하는 것입니다. 또한 때때로 Performance Budget Calculator와 같은 도구가 유용할 수 있습니다. 조금만 사용해 보세요.
성능 저하는 우리가 매일 직면하는 문제입니다. 우리는 애플리케이션이 빠르게 타오르도록 노력할 수 있지만 곧 우리가 시작한 곳에서 끝납니다.
"
귀하의 이익을 위해 경쟁자를 사용하십시오
황홀할 정도로 흥분한 곰에게서 우연히 도망친 적이 있다면 이 문제에서 벗어나기 위해 달리기에서 올림픽 챔피언이 될 필요가 없다는 것을 이미 알고 있을 것입니다. 당신은 다른 사람보다 조금 더 빨리해야합니다.
그래서 경쟁자 목록을 만드십시오. 동일한 유형의 프로젝트인 경우 일반적으로 서로 유사한 페이지 유형으로 구성됩니다. 예를 들어, 인터넷 상점의 경우 제품 목록, 제품 세부 정보 페이지, 장바구니, 결제 등이 있는 페이지일 수 있습니다.
- 경쟁자의 프로젝트에 대한 각 페이지 유형에서 선택한 측정항목의 값을 측정합니다.
- 프로젝트에서 동일한 측정항목을 측정합니다.
- 경쟁사의 프로젝트에서 각 메트릭에 대한 귀하의 값보다 더 나은 가장 가까운 것을 찾으십시오. 여기에 20%를 더하고 다음 목표로 설정합니다.
왜 20%인가? 이것은 육안으로 그 차이가 눈에 띄게 될 것이라는 것을 의미하는 마법의 숫자입니다. 이 수치에 대한 자세한 내용은 Denys Mishunov의 기사 "성능 인식이 중요한 이유, 1부: 시간 인식"에서 확인할 수 있습니다.
그림자와의 싸움
독특한 프로젝트가 있습니까? 경쟁자가 없습니까? 아니면 가능한 모든 면에서 이미 그들 중 누구보다 낫습니까? 문제가 아니다. 당신은 언제나 가치 있는 유일한 상대, 즉 자신과 경쟁할 수 있습니다. 각 페이지 유형에서 프로젝트의 각 성능 메트릭을 측정한 다음 동일한 20%만큼 개선합니다.
합성 테스트
성능을 측정하는 방법에는 두 가지가 있습니다.
- 합성 (통제된 환경에서)
- RUM (실제 사용자 측정)
프로덕션의 실제 사용자로부터 데이터가 수집되고 있습니다.
이 기사에서는 합성 테스트를 사용하고 프로젝트 배포를 위해 내장 CI와 함께 GitLab을 사용한다고 가정합니다.
라이브러리 및 메트릭으로서의 크기
라이브러리를 개발하여 NPM에 게시하기로 결정했다고 가정해 보겠습니다. 경쟁사보다 훨씬 가볍게 유지하여 결과 프로젝트의 최종 크기에 미치는 영향을 줄이려고 합니다. 이렇게 하면 클라이언트 트래픽(때로는 클라이언트가 비용을 지불하는 트래픽)이 절약됩니다. 또한 프로젝트를 더 빠르게 로드할 수 있습니다. 이는 모바일 점유율이 증가하고 연결 속도가 느리고 인터넷 범위가 파편화된 새로운 시장과 관련하여 매우 중요합니다.
라이브러리 크기 측정용 패키지
라이브러리의 크기를 가능한 한 작게 유지하려면 개발 시간에 따라 라이브러리가 어떻게 변하는지 주의 깊게 관찰해야 합니다. 하지만 어떻게 할 수 있습니까? 글쎄, 우리는 Evil Martians의 Andrey Sitnik이 만든 패키지 Size Limit를 사용할 수 있습니다.
설치합시다.
npm i -D size-limit @size-limit/preset-small-lib
그런 다음 package.json
에 추가합니다.
"scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],
"size-limit":[{},{},…]
블록에는 확인하려는 파일의 크기 목록이 포함되어 있습니다. 우리의 경우 index.js
라는 단일 파일입니다.
NPM 스크립트 size
는 앞에서 언급한 구성 블록 size-limit
을 읽고 거기에 나열된 파일 크기를 확인하는 크기 size-limit
패키지를 실행합니다. 실행하고 어떤 일이 일어나는지 봅시다.
npm run size

파일의 크기를 볼 수 있지만 이 크기는 실제로 제어되지 않습니다. package.json
에 limit
를 추가하여 수정하겠습니다.
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
이제 스크립트를 실행하면 우리가 설정한 제한에 대해 유효성이 검사됩니다.

새로운 개발에서 정의된 제한을 초과하는 지점까지 파일 크기가 변경되는 경우 스크립트는 0이 아닌 코드로 완료됩니다. 이것은 다른 것을 제외하고 GitLab CI에서 파이프라인을 중지한다는 것을 의미합니다.

이제 git hook을 사용하여 모든 커밋 전에 제한에 대해 파일 크기를 확인할 수 있습니다. husky 패키지를 사용하여 멋지고 간단한 방법으로 만들 수도 있습니다.
설치합시다.
npm i -D husky
그런 다음 package.json
을 수정합니다.
"size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },
이제 각 커밋이 자동으로 실행되기 전에 npm run size
명령이 실행되고 0이 아닌 코드로 끝나면 커밋이 발생하지 않습니다.

그러나 후크를 건너뛸 수 있는 방법이 많이 있으므로(의도적으로 또는 우발적으로) 너무 많이 의존해서는 안 됩니다.
또한 이 검사를 차단할 필요가 없다는 점에 유의하는 것이 중요합니다. 왜요? 새로운 기능을 추가하는 동안 라이브러리의 크기가 커지는 것은 괜찮기 때문입니다. 변경 사항을 표시해야 합니다. 그게 전부입니다. 이것은 필요하지 않은 도우미 라이브러리를 도입하여 실수로 크기가 증가하는 것을 방지하는 데 도움이 됩니다. 또한 개발자와 제품 소유자에게 추가되는 기능이 크기를 늘릴 가치가 있는지 여부를 고려할 이유를 제공할 수도 있습니다. 또는 더 작은 대안 패키지가 있는지 여부. Bundlephobia를 사용하면 거의 모든 NPM 패키지에 대한 대안을 찾을 수 있습니다.
그래서 우리는 무엇을해야합니까? 병합 요청에서 직접 파일 크기의 변화를 보여줍시다! 그러나 직접 마스터하도록 강요하지는 않습니다. 어른 개발자처럼 행동하지 않습니까?
GitLab CI에서 검사 실행
메트릭 유형의 GitLab 아티팩트를 추가해 보겠습니다. 아티팩트는 파이프라인 작업이 완료된 후 «라이브»될 파일입니다. 이 특정 유형의 아티팩트를 사용하면 병합 요청에 추가 위젯을 표시하여 마스터의 아티팩트와 기능 분기 간의 메트릭 값 변경을 표시할 수 있습니다. metrics
아티팩트의 형식은 텍스트 Prometheus 형식입니다. 아티팩트 내부의 GitLab 값의 경우 텍스트일 뿐입니다. GitLab은 값이 정확히 변경된 것이 무엇인지 이해하지 못합니다. 값이 다르다는 것만 알고 있습니다. 그럼 정확히 어떻게 해야 할까요?
- 파이프라인에서 아티팩트를 정의합니다.
- 파이프라인에 아티팩트를 생성하도록 스크립트를 변경합니다.
아티팩트를 생성하려면 .gitlab-ci.yml
을 다음과 같이 변경해야 합니다.
image: node:latest stages: - performance sizecheck: stage: performance before_script: - npm ci script: - npm run size + artifacts: + expire_in: 7 days + paths: + - metric.txt + reports: + metrics: metric.txt
-
expire_in: 7 days
— 아티팩트가 7일 동안 존재합니다. paths: metric.txt
루트 카탈로그에 저장됩니다. 이 옵션을 건너뛰면 다운로드할 수 없습니다.reports: metrics: metric.txt
아티팩트의 유형reports:metrics
입니다.
이제 크기 제한이 보고서를 생성하도록 합시다. 그렇게 하려면 package.json
을 변경해야 합니다.
"scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },
--json
키가 있는 size-limit
은 json 형식으로 데이터를 출력합니다.

size-limit --json
명령은 JSON을 콘솔에 출력합니다. JSON에는 파일 이름과 크기가 포함된 객체 배열이 포함되어 있을 뿐만 아니라 크기 제한을 초과하는지 알려줍니다. (큰 미리보기) 그리고 리디렉션 > size-limit.json
은 JSON을 size-limit.json
파일에 저장합니다.
이제 이로부터 아티팩트를 생성해야 합니다. 형식은 [metrics name][space][metrics value]
로 요약됩니다. generate-metric.js
스크립트를 생성해 보겠습니다.
const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);
그리고 package.json
에 추가하세요.
"scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },
post
접두사를 사용했기 때문에 npm run size
명령은 먼저 size
스크립트를 실행한 다음 자동으로 postsize
스크립트를 실행하여 아티팩트인 metric.txt
파일을 생성합니다.
결과적으로 이 분기를 마스터에 병합하고 무언가를 변경하고 새 병합 요청을 생성하면 다음이 표시됩니다.

페이지에 표시되는 위젯에서 먼저 메트릭 이름( size
) 다음에 기능 분기의 메트릭 값과 둥근 괄호 안의 마스터 값을 봅니다.
이제 패키지의 크기를 변경하고 병합할지 여부를 합리적으로 결정하는 방법을 실제로 볼 수 있습니다.
- 이 저장소에서 이 모든 코드를 볼 수 있습니다.
이력서
좋아요! 그래서 우리는 사소한 사건을 처리하는 방법을 알아 냈습니다. 파일이 여러 개인 경우 줄 바꿈으로 측정항목을 구분하면 됩니다. 크기 제한의 대안으로 번들 크기를 고려할 수 있습니다. WebPack을 사용하는 경우 --profile
및 --json
플래그로 빌드하여 필요한 모든 크기를 얻을 수 있습니다.
webpack --profile --json > stats.json
next.js를 사용하는 경우 @next/bundle-analyzer 플러그인을 사용할 수 있습니다. 그것은 당신에게 달려 있습니다!
등대 사용
Lighthouse는 프로젝트 분석의 사실상의 표준입니다. 성능, a11y, 모범 사례를 측정하고 SEO 점수를 제공할 수 있는 스크립트를 작성해 보겠습니다.
모든 것을 측정하는 스크립트
시작하려면 측정을 수행할 등대 패키지를 설치해야 합니다. 헤드리스 브라우저로 사용할 puppeter도 설치해야 합니다.
npm i -D lighthouse puppeteer
다음으로 lighthouse.js
스크립트를 만들고 브라우저를 시작하겠습니다.
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();
이제 주어진 URL을 분석하는 데 도움이 되는 함수를 작성해 보겠습니다.
const lighthouse = require('lighthouse'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => { const data = await lighthouse( `${DOMAIN}${url}`, { port: new URL(browser.wsEndpoint()).port, output: 'json', }, { extends: 'lighthouse:full', } ); const { report: reportJSON } = data; const report = JSON.parse(reportJSON); // … }
엄청난! 이제 브라우저 객체를 인수로 받아들이고 URL
을 인수로 받아들이고 해당 URL
을 lighthouse
에 전달한 후 보고서를 생성하는 함수를 반환하는 함수가 있습니다.
lighthouse
에 다음 인수를 전달합니다.
- 분석하려는 주소
-
lighthouse
옵션, 특히 브라우저port
및output
(보고서의 출력 형식) -
report
구성 및lighthouse:full
(측정할 수 있는 모든 것). 보다 정확한 구성은 설명서를 확인하십시오.
아주 멋진! 이제 보고서가 있습니다. 그러나 우리는 그것으로 무엇을 할 수 있습니까? 음, 파이프라인을 중지할 0이 아닌 코드로 제한 및 종료 스크립트에 대한 메트릭을 확인할 수 있습니다.
if (report.categories.performance.score < 0.8) process.exit(1);
그러나 성능을 가시적이고 비차단적으로 만들고 싶습니까? 그런 다음 다른 아티팩트 유형인 GitLab 성능 아티팩트를 채택해 보겠습니다.
GitLab 성능 아티팩트
이 아티팩트 형식을 이해하려면 sitespeed.io 플러그인의 코드를 읽어야 합니다. (왜 GitLab은 자체 문서 내에서 아티팩트의 형식을 설명할 수 없습니까? 미스터리입니다. )
[ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]
아티팩트는 객체의 배열을 포함하는 JSON
파일입니다. 각각은 하나의 URL
에 대한 보고서를 나타냅니다.
[{page 1}, {page 2}, …]
각 페이지는 다음 속성을 가진 개체로 표시됩니다.
-
subject
페이지 식별자(이러한 경로 이름을 사용하는 것이 매우 편리합니다); -
metrics
개체의 배열(각각은 페이지에서 이루어진 하나의 측정을 나타냄).
{ "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }
measurement
은 다음 속성을 포함하는 개체입니다.
-
name
측정 이름, 예를 들어Time to first byte
또는Time to interactive
. -
value
수치 측정 결과. -
desiredSize
대상 값이 가능한 한 작아야 하는 경우(예:Time to interactive
측정항목의 경우) 값은smaller
합니다. 예를 들어 등대Performance score
의 경우와 같이 가능한 한 커야 하는 경우larger
값을 사용합니다.
{ "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }
표준 등대 메트릭이 있는 한 페이지에 대한 보고서를 반환하는 방식으로 buildReport
함수를 수정해 보겠습니다.

const buildReport = browser => async url => { // … const metrics = [ { name: report.categories.performance.title, value: report.categories.performance.score, desiredSize: 'larger', }, { name: report.categories.accessibility.title, value: report.categories.accessibility.score, desiredSize: 'larger', }, { name: report.categories['best-practices'].title, value: report.categories['best-practices'].score, desiredSize: 'larger', }, { name: report.categories.seo.title, value: report.categories.seo.score, desiredSize: 'larger', }, { name: report.categories.pwa.title, value: report.categories.pwa.score, desiredSize: 'larger', }, ]; return { subject: url, metrics: metrics, }; }
이제 보고서를 생성하는 함수가 있습니다. 프로젝트의 각 페이지 유형에 적용해 보겠습니다. 먼저, process.env.DOMAIN
에 스테이징 도메인이 포함되어야 함을 명시해야 합니다(미리 기능 분기에서 프로젝트를 배포해야 함).
+ const fs = require('fs'); const lighthouse = require('lighthouse'); const puppeteer = require('puppeteer'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => {/* … */}; + const urls = [ + '/inloggen', + '/wachtwoord-herstellen-otp', + '/lp/service', + '/send-request-to/ww-tammer', + '/post-service-request/binnenschilderwerk', + ]; (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + const metrics = await builder(url); + report.push(metrics); + } + fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + await browser.close(); })();
- 이 요지에서 전체 소스를 찾을 수 있고 이 저장소에서 작업 예제를 찾을 수 있습니다.
참고 : 이 시점에서 "왜 내 시간을 뺏어? Promise.all을 제대로 사용할 수도 없습니다!" 제 변호를 하자면, 동시에 둘 이상의 등대 인스턴스를 실행하는 것은 측정 결과의 정확도에 부정적인 영향을 미치기 때문에 권장되지 않습니다. 또한 상당한 독창성을 보여주지 않으면 예외가 발생합니다.
다중 프로세스 사용
아직도 병렬 측정을 하고 계십니까? 좋습니다. 노드 클러스터(또는 굵게 표시하는 것을 좋아하는 경우 작업자 스레드)를 사용할 수 있지만 사용 가능한 여러 코어가 있는 환경에서 파이프라인이 실행되는 경우에만 논의하는 것이 좋습니다. 그런 다음에도 Node.js의 특성 때문에 각 프로세스 포크에서 전체 가중치 Node.js 인스턴스가 생성된다는 점을 염두에 두어야 합니다(같은 인스턴스를 재사용하는 대신 RAM 소비 증가로 이어짐). 이 모든 것은 하드웨어 요구 사항이 증가하고 속도가 조금 더 빨라지기 때문에 비용이 더 많이 든다는 것을 의미합니다. 게임이 양초의 가치가 없는 것처럼 보일 수 있습니다.
위험을 감수하려면 다음을 수행해야 합니다.
- URL 배열을 코어 번호별로 청크로 분할합니다.
- 코어 수에 따라 프로세스의 포크를 생성합니다.
- 어레이의 일부를 포크로 전송한 다음 생성된 보고서를 검색합니다.
배열을 분할하려면 다중 파일 접근 방식을 사용할 수 있습니다. 몇 분 만에 작성된 다음 코드는 다른 코드보다 나쁘지 않습니다.
/** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }
/** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }
코어 수에 따라 포크 만들기:

// Adding packages that allow us to use cluster const cluster = require('cluster'); // And find out how many cors are available. Both packages are build-in for node.js. const numCPUs = require('os').cpus().length; (async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { // Creating child processes const worker = cluster.fork(); }); } else { // Child process } })();
청크 배열을 자식 프로세스로 전송하고 보고서를 다시 검색해 보겠습니다.
(async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { const worker = cluster.fork(); + // Send message with URL's array to child process + worker.send(chunk); }); } else { // Child process + // Recieveing message from parent proccess + process.on('message', async (urls) => { + const browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], + }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + // Generating report for each URL + const metrics = await builder(url); + report.push(metrics); + } + // Send array of reports back to the parent proccess + cluster.worker.send(report); + await browser.close(); + }); } })();
그리고 마지막으로 보고서를 하나의 어레이로 재조립하고 아티팩트를 생성합니다.
- 여러 프로세스에서 등대를 사용하는 방법을 보여주는 예제와 함께 전체 코드와 저장소를 확인하십시오.
측정 정확도
글쎄, 우리는 측정을 병렬화하여 이미 불행한 lighthouse
의 큰 측정 오류를 증가 시켰습니다. 하지만 어떻게 줄일 수 있습니까? 글쎄, 몇 가지 측정을하고 평균을 계산합니다.
이를 위해 현재 측정 결과와 이전 측정 결과 간의 평균을 계산하는 함수를 작성합니다.
// Count of measurements we want to make const MEASURES_COUNT = 3; /* * Reducer which will calculate an avarage value of all page measurements * @param pages {Object} — accumulator * @param page {Object} — page * @return {Object} — page with avarage metrics values */ const mergeMetrics = (pages, page) => { if (!pages) return page; return { subject: pages.subject, metrics: pages.metrics.map((measure, index) => { let value = (measure.value + page.metrics[index].value)/2; value = +value.toFixed(2); return { ...measure, value, } }), } }
그런 다음 이를 사용하도록 코드를 변경합니다.
process.on('message', async (urls) => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); const builder = buildReport(browser); const report = []; for (let url of urls) { + // Let's measure MEASURES_COUNT times and calculate the avarage + let measures = []; + let index = MEASURES_COUNT; + while(index--){ const metric = await builder(url); + measures.push(metric); + } + const measure = measures.reduce(mergeMetrics); report.push(measure); } cluster.worker.send(report); await browser.close(); }); }
- 전체 코드가 포함된 요지와 예제를 통해 리포지토리를 확인하세요.
이제 파이프라인에 lighthouse
를 추가할 수 있습니다.
파이프라인에 추가하기
먼저 .gitlab-ci.yml
이라는 구성 파일을 만듭니다.
image: node:latest stages: # You need to deploy a project to staging and put the staging domain name # into the environment variable DOMAIN. But this is beyond the scope of this article, # primarily because it is very dependent on your specific project. # - deploy # - performance lighthouse: stage: performance before_script: - apt-get update - apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget - npm ci script: - node lighthouse.js artifacts: expire_in: 7 days paths: - performance.json reports: performance: performance.json
puppeteer
에는 설치된 여러 패키지가 필요합니다. 대안으로 docker
사용을 고려할 수 있습니다. 그 외에도 아티팩트의 유형을 성능으로 설정한다는 사실이 의미가 있습니다. 그리고 master 브랜치와 feature 브랜치 모두에 이를 갖게 되는 즉시 병합 요청에 다음과 같은 위젯이 표시됩니다.

멋진?
이력서
우리는 마침내 더 복잡한 경우를 끝냈습니다. 분명히 등대 외에도 유사한 도구가 여러 개 있습니다. 예를 들어, sitespeed.io. GitLab 문서에는 GitLab의 파이프라인에서 sitespeed
를 사용하는 방법을 설명하는 기사도 포함되어 있습니다. 아티팩트를 생성할 수 있는 GitLab용 플러그인도 있습니다. 그러나 누가 기업 괴물이 소유한 제품보다 커뮤니티 주도의 오픈 소스 제품을 선호할까요?
악인에게는 휴식이 없다
우리가 마침내 거기에 있는 것처럼 보일지 모르지만, 아직 아닙니다. 유료 GitLab 버전을 사용하는 경우 보고서 유형 metrics
및 performance
이 포함된 아티팩트가 각 사용자에 대해 월 $19의 premium
및 silver
부터 시작하는 계획에 있습니다. 또한 필요한 특정 기능만 구매할 수 없으며 요금제만 변경할 수 있습니다. 죄송합니다. 그래서 우리는 무엇을 할 수 있습니까? Checks API 및 Status API가 있는 GitHub와 달리 GitLab은 병합 요청에서 실제 위젯을 직접 생성하는 것을 허용하지 않습니다. 그리고 곧 얻을 희망도 없습니다.

이러한 기능을 실제로 지원하는지 확인하는 한 가지 방법: 파이프라인에서 환경 변수 GITLAB_FEATURES
를 검색할 수 있습니다. 목록에 merge_request_performance_metrics
및 metrics_reports
가 없으면 이 기능이 지원되지 않습니다.
GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics, elastic_search, export_issues,group_bulk_edit,group_burndown_charts,group_webhooks, issuable_default_templates,issue_board_focus_mode,issue_weights,jenkins_integration, ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees, multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users, push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board, usage_quotas,visual_review_app,wip_limits
지원이 없다면 우리는 뭔가를 생각해 내야 합니다. 예를 들어, 병합 요청에 주석을 추가하고 필요한 모든 데이터가 포함된 테이블에 주석을 추가할 수 있습니다. 코드를 그대로 둘 수 있습니다. 아티팩트가 생성되지만 위젯은 항상 «metrics are unchanged»
메시지를 표시합니다.
매우 이상하고 명백하지 않은 행동; 무슨 일이 일어나고 있는지 이해하려면 신중하게 생각해야 했습니다.
그래서, 계획은 무엇입니까?
-
master
브랜치에서 아티팩트를 읽어야 합니다. -
markdown
형식으로 주석을 작성하십시오. - 현재 기능 분기에서 마스터로의 병합 요청 식별자를 가져옵니다.
- 댓글을 추가합니다.
마스터 브랜치에서 아티팩트를 읽는 방법
master
브랜치와 기능 브랜치 간에 성능 메트릭이 어떻게 변경되는지 보여주고 싶다면 master
에서 아티팩트를 읽어야 합니다. 그렇게 하려면 fetch
를 사용해야 합니다.
npm i -S isomorphic-fetch
// You can use predefined CI environment variables // @see https://gitlab.com/help/ci/variables/predefined_variables.md // We need fetch polyfill for node.js const fetch = require('isomorphic-fetch'); // GitLab domain const GITLAB_DOMAIN = process.env.CI_SERVER_HOST || process.env.GITLAB_DOMAIN || 'gitlab.com'; // User or organization name const NAME_SPACE = process.env.CI_PROJECT_NAMESPACE || process.env.PROJECT_NAMESPACE || 'silentimp'; // Repo name const PROJECT = process.env.CI_PROJECT_NAME || process.env.PROJECT_NAME || 'lighthouse-comments'; // Name of the job, which create an artifact const JOB_NAME = process.env.CI_JOB_NAME || process.env.JOB_NAME || 'lighthouse'; /* * Returns an artifact * * @param name {String} - artifact file name * @return {Object} - object with performance artifact * @throw {Error} - thhrow an error, if artifact contain string, that can't be parsed as a JSON. Or in case of fetch errors. */ const getArtifact = async name => { const response = await fetch(`https://${GITLAB_DOMAIN}/${NAME_SPACE}/${PROJECT}/-/jobs/artifacts/master/raw/${name}?job=${JOB_NAME}`); if (!response.ok) throw new Error('Artifact not found'); const data = await response.json(); return data; };
주석 텍스트 만들기
markdown
형식으로 주석 텍스트를 작성해야 합니다. 도움이 될 몇 가지 서비스 기능을 만들어 보겠습니다.
/** * Return part of report for specific page * * @param report {Object} — report * @param subject {String} — subject, that allow find specific page * @return {Object} — page report */ const getPage = (report, subject) => report.find(item => (item.subject === subject)); /** * Return specific metric for the page * * @param page {Object} — page * @param name {String} — metrics name * @return {Object} — metric */ const getMetric = (page, name) => page.metrics.find(item => item.name === name); /** * Return table cell for desired metric * * @param branch {Object} - report from feature branch * @param master {Object} - report from master branch * @param name {String} - metrics name */ const buildCell = (branch, master, name) => { const branchMetric = getMetric(branch, name); const masterMetric = getMetric(master, name); const branchValue = branchMetric.value; const masterValue = masterMetric.value; const desiredLarger = branchMetric.desiredSize === 'larger'; const isChanged = branchValue !== masterValue; const larger = branchValue > masterValue; if (!isChanged) return `${branchValue}`; if (larger) return `${branchValue} ${desiredLarger ? '' : '' } **+${Math.abs(branchValue - masterValue).toFixed(2)}**`; return `${branchValue} ${!desiredLarger ? '' : '' } **-${Math.abs(branchValue - masterValue).toFixed(2)}**`; }; /** * Returns text of the comment with table inside * This table contain changes in all metrics * * @param branch {Object} report from feature branch * @param master {Object} report from master branch * @return {String} comment markdown */ const buildCommentText = (branch, master) =>{ const md = branch.map( page => { const pageAtMaster = getPage(master, page.subject); if (!pageAtMaster) return ''; const md = `|${page.subject}|${buildCell(page, pageAtMaster, 'Performance')}|${buildCell(page, pageAtMaster, 'Accessibility')}|${buildCell(page, pageAtMaster, 'Best Practices')}|${buildCell(page, pageAtMaster, 'SEO')}| `; return md; }).join(''); return ` |Path|Performance|Accessibility|Best Practices|SEO| |--- |--- |--- |--- |--- | ${md} `; };
주석을 작성하는 스크립트
GitLab API를 사용하려면 토큰이 필요합니다. 하나를 생성하려면 GitLab을 열고 로그인하고 메뉴의 '설정' 옵션을 연 다음 탐색 메뉴 왼쪽에 있는 '액세스 토큰'을 열어야 합니다. 그런 다음 토큰을 생성할 수 있는 양식을 볼 수 있어야 합니다.

또한 프로젝트 ID가 필요합니다. 저장소 '설정'(하위 메뉴 '일반')에서 찾을 수 있습니다.

병합 요청에 주석을 추가하려면 해당 ID를 알아야 합니다. 병합 요청 ID를 얻을 수 있는 함수는 다음과 같습니다.
// You can set environment variables via CI/CD UI. // @see https://gitlab.com/help/ci/variables/README#variables // I have set GITLAB_TOKEN this way // ID of the project const GITLAB_PROJECT_ID = process.env.CI_PROJECT_ID || '18090019'; // Token const TOKEN = process.env.GITLAB_TOKEN; /** * Returns iid of the merge request from feature branch to master * @param from {String} — name of the feature branch * @param to {String} — name of the master branch * @return {Number} — iid of the merge request */ const getMRID = async (from, to) => { const response = await fetch(`https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests?target_branch=${to}&source_branch=${from}`, { method: 'GET', headers: { 'PRIVATE-TOKEN': TOKEN, } }); if (!response.ok) throw new Error('Merge request not found'); const [{iid}] = await response.json(); return iid; };
We need to get a feature branch name. You may use the environment variable CI_COMMIT_REF_SLUG
inside the pipeline. Outside of the pipeline, you can use the current-git-branch
package. Also, you will need to form a message body.
Let's install the packages we need for this matter:
npm i -S current-git-branch form-data
And now, finally, function to add a comment:
const FormData = require('form-data'); const branchName = require('current-git-branch'); // Branch from which we are making merge request // In the pipeline we have environment variable `CI_COMMIT_REF_NAME`, // which contains name of this banch. Function `branchName` // will return something like «HEAD detached» message in the pipeline. // And name of the branch outside of pipeline const CURRENT_BRANCH = process.env.CI_COMMIT_REF_NAME || branchName(); // Merge request target branch, usually it's master const DEFAULT_BRANCH = process.env.CI_DEFAULT_BRANCH || 'master'; /** * Adding comment to merege request * @param md {String} — markdown text of the comment */ const addComment = async md => { const iid = await getMRID(CURRENT_BRANCH, DEFAULT_BRANCH); const commentPath = `https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests/${iid}/notes`; const body = new FormData(); body.append('body', md); await fetch(commentPath, { method: 'POST', headers: { 'PRIVATE-TOKEN': TOKEN, }, body, }); };
And now we can generate and add a comment:
cluster.on('message', (worker, msg) => { report = [...report, ...msg]; worker.disconnect(); reportsCount++; if (reportsCount === chunks.length) { fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + if (CURRENT_BRANCH === DEFAULT_BRANCH) process.exit(0); + try { + const masterReport = await getArtifact('performance.json'); + const md = buildCommentText(report, masterReport) + await addComment(md); + } catch (error) { + console.log(error); + } process.exit(0); } });
- Check the gist and demo repository.
Now create a merge request and you will get:

이력서
Comments are much less visible than widgets but it's still much better than nothing. This way we can visualize the performance even without artifacts.
입증
OK, but what about authentication? The performance of the pages that require authentication is also important. It's easy: we will simply log in. puppeteer
is essentially a fully-fledged browser and we can write scripts that mimic user actions:
const LOGIN_URL = '/login'; const USER_EMAIL = process.env.USER_EMAIL; const USER_PASSWORD = process.env.USER_PASSWORD; /** * Authentication sctipt * @param browser {Object} — browser instance */ const login = async browser => { const page = await browser.newPage(); page.setCacheEnabled(false); await page.goto(`${DOMAIN}${LOGIN_URL}`, { waitUntil: 'networkidle2' }); await page.click('input[name=email]'); await page.keyboard.type(USER_EMAIL); await page.click('input[name=password]'); await page.keyboard.type(USER_PASSWORD); await page.click('button[data-test]', { waitUntil: 'domcontentloaded' }); };
Before checking a page that requires authentication, we may just run this script. 완료.
요약
In this way, I built the performance monitoring system at Werkspot — a company I currently work for. It's great when you have the opportunity to experiment with the bleeding edge technology.
Now you also know how to visualize performance change, and it's sure to help you better track performance degradation. But what comes next? You can save the data and visualize it for a time period in order to better understand the big picture, and you can collect performance data directly from the users.
You may also check out a great talk on this subject: “Measuring Real User Performance In The Browser.” When you build the system that will collect performance data and visualize them, it will help to find your performance bottlenecks and resolve them. 좋은 결과 내길 바랄 게!