كيفية جعل الأداء مرئيًا باستخدام GitLab CI و Hoodoo من مصنوعات GitLab
نشرت: 2022-03-10تدهور الأداء مشكلة نواجهها على أساس يومي. يمكننا أن نبذل جهدًا لجعل التطبيق سريعًا ، ولكن سرعان ما ننتهي من حيث بدأنا. يحدث ذلك بسبب إضافة ميزات جديدة وحقيقة أننا في بعض الأحيان لا نفكر في الحزم التي نضيفها ونحدّثها باستمرار ، أو نفكر في مدى تعقيد الكود الخاص بنا. إنه شيء صغير بشكل عام ، لكنه لا يزال يتعلق بالأشياء الصغيرة.
لا يمكننا تحمل وجود تطبيق بطيء. الأداء هو ميزة تنافسية يمكن أن تجلب العملاء والاحتفاظ بهم. لا يمكننا تحمل قضاء الوقت بانتظام في تحسين التطبيقات مرة أخرى. إنها مكلفة ومعقدة. وهذا يعني أنه على الرغم من كل مزايا الأداء من منظور الأعمال ، إلا أنه بالكاد يكون مربحًا. كخطوة أولى للوصول إلى حل لأي مشكلة ، نحتاج إلى توضيح المشكلة. هذه المقالة سوف تساعدك في ذلك بالضبط.
ملاحظة : إذا كان لديك فهم أساسي لـ Node.js ، وهي فكرة غامضة حول كيفية عمل CI / CD الخاص بك ، وتهتم بأداء التطبيق أو مزايا العمل التي يمكن أن يجلبها ، فنحن على ما يرام.
كيفية إنشاء ميزانية أداء لمشروع
الأسئلة الأولى التي يجب أن نطرحها على أنفسنا هي:
"ما هو مشروع الأداء؟"
"ما المقاييس التي يجب أن أستخدمها؟"
"ما هي قيم هذه المقاييس المقبولة؟"
يقع اختيار المقاييس خارج نطاق هذه المقالة ويعتمد بشكل كبير على سياق المشروع ، لكنني أوصي بأن تبدأ بقراءة مقاييس الأداء المرتكزة على المستخدم بواسطة Philip Walton.
من وجهة نظري ، من الجيد استخدام حجم المكتبة بالكيلو بايت كمقياس لحزمة npm. لماذا ا؟ حسنًا ، هذا لأنه إذا قام أشخاص آخرون بتضمين الكود الخاص بك في مشاريعهم ، فقد يرغبون في تقليل تأثير الكود الخاص بك على الحجم النهائي لتطبيقهم.
بالنسبة للموقع ، سأعتبر Time To First Byte (TTFB) كمقياس. يوضح هذا المقياس مقدار الوقت الذي يستغرقه الخادم للاستجابة بشيء ما. هذا المقياس مهم ، لكنه غامض تمامًا لأنه يمكن أن يتضمن أي شيء - بدءًا من وقت عرض الخادم وانتهاءً بمشكلات زمن الانتقال. لذلك من الجيد استخدامه جنبًا إلى جنب مع توقيت الخادم أو OpenTracing لمعرفة ما يتكون منه بالضبط.
يجب عليك أيضًا التفكير في مقاييس مثل Time to Interactive (TTI) و First Meaningful Paint (سيتم استبدال الأخير قريبًا بـ Largest Contentful Paint (LCP)). أعتقد أن كلاهما أكثر أهمية - من منظور الأداء المتصور.
لكن ضع في اعتبارك: المقاييس مرتبطة دائمًا بالسياق ، لذا من فضلك لا تأخذ هذا كأمر مسلم به. فكر فيما هو مهم في حالتك الخاصة.
أسهل طريقة لتحديد القيم المرغوبة للمقاييس هي استخدام منافسيك - أو حتى نفسك. أيضًا ، من وقت لآخر ، قد تكون أدوات مثل حاسبة ميزانية الأداء مفيدة - ما عليك سوى التلاعب بها قليلاً.
تدهور الأداء مشكلة نواجهها يوميًا. يمكننا بذل جهد لجعل التطبيق سريعًا ، ولكن سرعان ما ننتهي من حيث بدأنا.
"
استخدم المنافسين لصالحك
إذا حدث أن هربت من دب متحمس للغاية ، فأنت تعلم بالفعل أنك لست بحاجة إلى أن تكون بطلاً أولمبيًا في الجري للخروج من هذه المشكلة. أنت فقط بحاجة إلى أن تكون أسرع قليلاً من الرجل الآخر.
لذلك قم بعمل قائمة بالمنافسين. إذا كانت هذه مشروعات من نفس النوع ، فعادة ما تتكون من أنواع صفحات متشابهة مع بعضها البعض. على سبيل المثال ، بالنسبة لمتجر عبر الإنترنت ، قد تكون صفحة بها قائمة منتجات وصفحة تفاصيل المنتج وعربة تسوق وسداد رسوم وما إلى ذلك.
- قياس قيم المقاييس التي اخترتها في كل نوع من الصفحات لمشاريع منافسك ؛
- قم بقياس نفس المقاييس في مشروعك ؛
- ابحث عن الأقرب أفضل من قيمتك لكل مقياس في مشاريع المنافس. إضافة 20٪ إليهم وتحديد أهدافك التالية.
لماذا 20٪؟ هذا رقم سحري يفترض أنه يعني أن الفرق سيكون ملحوظًا للعين المجردة. يمكنك قراءة المزيد عن هذا الرقم في مقال دينيس ميشونوف "لماذا يهم الأداء المتصور ، الجزء 1: إدراك الوقت".
قتال بظل
هل لديك مشروع فريد؟ ليس لديك أي منافسين؟ أم أنك بالفعل أفضل من أي منهم بكل المعاني الممكنة؟ إنها ليست مشكلة. يمكنك دائمًا التنافس مع الخصم الوحيد الجدير ، أي نفسك. قم بقياس كل مقياس أداء لمشروعك في كل نوع من أنواع الصفحات ثم قم بتحسينها بنفس النسبة البالغة 20٪.
الاختبارات التركيبية
هناك طريقتان لقياس الأداء:
- اصطناعي (في بيئة خاضعة للرقابة)
- RUM (قياسات المستخدم الحقيقي)
يتم جمع البيانات من مستخدمين حقيقيين في الإنتاج.
في هذه المقالة ، سوف نستخدم الاختبارات التركيبية ونفترض أن مشروعنا يستخدم GitLab مع CI المدمج الخاص به لنشر المشروع.
المكتبة وحجمها متري
لنفترض أنك قررت تطوير مكتبة ونشرها على NPM. تريد أن تبقيها خفيفة - أخف بكثير من المنافسين - لذلك يكون لها تأثير أقل على الحجم النهائي للمشروع الناتج. هذا يوفر حركة مرور العملاء - في بعض الأحيان حركة المرور التي يدفع العميل مقابلها. كما أنه يسمح بتحميل المشروع بشكل أسرع ، وهو أمر مهم للغاية فيما يتعلق بحصة المحمول المتزايدة والأسواق الجديدة بسرعات اتصال بطيئة وتغطية إنترنت مجزأة.
حزمة لقياس حجم المكتبة
للحفاظ على حجم المكتبة صغيرًا قدر الإمكان ، نحتاج إلى أن نراقب بعناية كيف تتغير بمرور وقت التطوير. لكن كيف يمكنك فعل ذلك؟ حسنًا ، يمكننا استخدام حد حجم الحزمة الذي أنشأه Andrey Sitnik من Evil Martians.
لنقم بتثبيته.
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
.
يعمل size
البرنامج النصي NPM فقط على تشغيل حزمة size-limit
، والتي تقرأ size-limit
كتلة التكوين المذكورة من قبل وتتحقق من حجم الملفات المدرجة هناك. لنشغلها ونرى ما سيحدث:
npm run size

يمكننا أن نرى حجم الملف ، لكن هذا الحجم ليس تحت السيطرة في الواقع. دعنا نصلح ذلك عن طريق إضافة limit
إلى package.json
:
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
الآن إذا قمنا بتشغيل البرنامج النصي ، فسيتم التحقق من صحته مقابل الحد الذي حددناه.

في حالة قيام التطوير الجديد بتغيير حجم الملف إلى نقطة تجاوز الحد المحدد ، فسيتم إكمال البرنامج النصي برمز غير صفري. هذا ، بصرف النظر عن الأشياء الأخرى ، يعني أنه سيوقف خط الأنابيب في 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
، وإذا كان سينتهي بكود غير صفري ، فلن يحدث الالتزام أبدًا.

ولكن هناك العديد من الطرق لتخطي الخطافات (عن قصد أو حتى عن طريق الصدفة) ، لذلك لا ينبغي أن نعتمد عليها كثيرًا.
أيضًا ، من المهم ملاحظة أننا لا نحتاج إلى جعل هذا الشيك محظورًا. لماذا ا؟ لأنه من المقبول أن يزداد حجم المكتبة أثناء إضافة ميزات جديدة. نحن بحاجة إلى جعل التغييرات مرئية ، هذا كل شيء. سيساعد هذا في تجنب زيادة الحجم بشكل عرضي بسبب تقديم مكتبة مساعدة لسنا بحاجة إليها. وربما يمنح المطورين ومالكي المنتجات سببًا للنظر فيما إذا كانت الميزة التي تتم إضافتها تستحق زيادة الحجم. أو ربما ، ما إذا كانت هناك حزم بديلة أصغر. يسمح لنا Bundlephobia بإيجاد بديل لأي حزمة NPM تقريبًا.
اذا ماذا يجب ان نفعل؟ دعنا نظهر التغيير في حجم الملف مباشرة في طلب الدمج! لكنك لا تضغط من أجل الإتقان مباشرة ؛ أنت تتصرف كمطور راشد ، أليس كذلك؟
إجراء فحصنا على GitLab CI
دعنا نضيف أداة GitLab من نوع المقاييس. الأداة عبارة عن ملف "يعيش" بعد انتهاء عملية خط الأنابيب. يسمح لنا هذا النوع المحدد من القطع الأثرية بإظهار عنصر واجهة مستخدم إضافي في طلب الدمج ، مع إظهار أي تغيير في قيمة المقياس بين الأداة الرئيسية وفرع الميزة. تنسيق عنصر metrics
هو تنسيق بروميثيوس النصي. بالنسبة لقيم 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 ." },
size-limit
مع مفتاح --json
البيانات بتنسيق json:

size-limit --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-analzer. الأمر متروك لك!
باستخدام المنارة
Lighthouse هي المعيار الفعلي في تحليلات المشروع. دعنا نكتب نصًا يسمح لنا بقياس الأداء ، و a11y ، وأفضل الممارسات ، وتزويدنا بنتيجة تحسين محركات البحث.
البرنامج النصي لقياس كل الأشياء
للبدء ، نحتاج إلى تثبيت حزمة المنارة التي ستجري القياسات. نحتاج أيضًا إلى تثبيت محرك الدمى الذي سنستخدمه كمتصفح بدون رأس.
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
(كل ما يمكننا قياسه). للحصول على تكوين أكثر دقة ، تحقق من الوثائق.
رائع! لدينا الآن تقريرنا. لكن ماذا يمكننا أن نفعل بها؟ حسنًا ، يمكننا التحقق من المقاييس مقابل الحدود والخروج من البرنامج النصي برمز غير صفري والذي سيوقف خط الأنابيب:
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 كامل الوزن يتم إنتاجه في كل عملية مفترقة (بدلاً من إعادة استخدام نفس الشيء الذي سيؤدي إلى زيادة استهلاك ذاكرة الوصول العشوائي). كل هذا يعني أنه سيكون أكثر تكلفة بسبب متطلبات الأجهزة المتزايدة وأسرع قليلاً. قد يبدو أن اللعبة لا تستحق كل هذا العناء.
إذا كنت تريد المخاطرة ، فستحتاج إلى:
- قم بتقسيم مجموعة 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
. بصرف النظر عن ذلك ، فمن المنطقي أن نحدد نوع الأداة كأداء. وبمجرد الحصول على الفرع الرئيسي والفرع المميز ، سترى عنصر واجهة مستخدم مثل هذا في طلب الدمج:

لطيف - جيد؟
سيرة ذاتية
لقد انتهينا أخيرًا من حالة أكثر تعقيدًا. من الواضح أن هناك العديد من الأدوات المماثلة بخلاف المنارة. على سبيل المثال ، sitespeed.io. تحتوي وثائق GitLab على مقال يشرح كيفية استخدام سرعة الموقع في خط أنابيب sitespeed
. هناك أيضًا مكون إضافي لـ GitLab يتيح لنا إنشاء عنصر. ولكن من الذي يفضل المنتجات مفتوحة المصدر التي يقودها المجتمع على تلك التي يملكها وحش الشركة؟
لا راحة للأشرار
قد يبدو أننا وصلنا أخيرًا ، لكن لا ، ليس بعد. إذا كنت تستخدم إصدارًا مدفوعًا من GitLab ، فإن القطع الأثرية مع metrics
أنواع التقارير performance
موجودة في الخطط بدءًا من premium
silver
الذي يكلف 19 دولارًا شهريًا لكل مستخدم. أيضًا ، لا يمكنك شراء ميزة معينة تحتاجها فقط - يمكنك فقط تغيير الخطة. آسف. ذلك ما يمكن أن نفعله؟ بخلاف GitHub مع Checks API و Status API ، لن يسمح لك 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 ، وتسجيل الدخول ، وفتح خيار "الإعدادات" في القائمة ، ثم فتح "رموز الوصول" الموجودة على الجانب الأيسر من قائمة التنقل. يجب أن تكون قادرًا بعد ذلك على رؤية النموذج ، والذي يسمح لك بإنشاء الرمز المميز.

أيضا ، سوف تحتاج إلى معرف للمشروع. يمكنك العثور عليه في "الإعدادات" في المستودع (في القائمة الفرعية "عام"):

لإضافة تعليق على طلب الدمج ، نحتاج إلى معرفة المعرف الخاص به. تبدو الوظيفة التي تتيح لك الحصول على معرّف طلب الدمج كما يلي:
// 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. حظا جيدا في ذلك!