في 2 فبراير 2017 الماضي، كتبت آن تايلور مقال 12 شيئاً نحتاج لرؤيتها من منشئي صفحات ووردبريس في 2017 + من قام بالأمر بالشكل الصحيح بالفعل في CodeInWP. لقد عملنا على تحسين Page Builder Sandwich باستمرار لأكثر من عام حتى الآن: نجري أبحاثنا، ونستمع إلى عملائنا، لذلك اعتقدنا أن Page Builder Sandwich سيصل إلى قائمة مقال “من قام بالأمر بالشكل الصحيح” في CodeInWP. ومع ذلك، لم نصل إليها بسبب الأداء.
لذا سألنا آن عن ذلك، وأجابت:
…لقد جربت الإصدار المجاني قليلاً. إنه يُظهر الكثير من الإمكانات الواعدة: العديد من الوحدات، وواجهة مستخدم بديهية للغاية، وتجربة رائعة لما تراه هو ما تحصل عليه (WYSIWYG)، وكل شيء يتم سحبه بسهولة. ومع ذلك، عندما كان لدي حوالي 10 وحدات على الصفحة، أصبح من الصعب الاستمرار في العمل معه بسبب بطء سرعة التحرير…
وفقاً لها، فإن Page Builder Sandwich يعاني من مشاكل في الأداء. وبما أننا نستمع دائماً إلى الملاحظات، فقد قمنا بالتحقق من الأمر.
اختبار الأداء
كنا بحاجة إلى طريقة للتحقق من أداء PBS؛ وبما أن PBS يعمل في الغالب على المتصفح عبر Javascript، فقد كانت أداة تحليل الجدول الزمني في Chrome (Timeline profiler) هي الأداة المناسبة لهذه المهمة. يمكن لأداة تحليل المخطط الزمني تحديد وقياس العمليات التي تحدث في صفحة الويب أثناء وقت التشغيل، وهو أمر مثالي للعثور على اختناقات السرعة.
رأينا أنه ينبغي علينا اختبار أبسط الخطوات أولاً – تحريك الفأرة عبر عدة عناصر وصفوف. أمر بسيط، أليس كذلك؟ كنا واثقين من أننا لن نواجه مشكلات في الأداء مع شيء بسيط كهذا. ولكن لسوء الحظ، كنا مخطئين.
بمجرد أن ضغطنا على زر التسجيل، قمنا بتحريك الفأرة لمدة ثانيتين تقريباً على الشاشة، ثم ضغطنا على زر الإيقاف. إليكم المخطط الزمني الناتج من أداة التحليل:
وانظر إلى هذا! إنه مليء باختناقات الأداء حتى الحافة!
هناك الكثير من الأمور التي تحدث في الرسم البياني، ولكن المشكلة الأكثر وضوحاً هي وجود الكثير من المناطق الأرجوانية فيه (وهو ما لا ينبغي أن يكون). علاوة على ذلك، فإن هذه المناطق الأرجوانية عريضة جداً بشكل ملحوظ، ويبدو أنها جميعاً تصل إلى ذروة الرسم البياني. ستلاحظ أيضاً أن الرسم البياني ممتلئ دائماً، وأن المناطق الأرجوانية لا تنخفض أبداً تقريباً إلى الصفر. يبدو الأمر كما لو أن المتصفح مشغول دائماً بفعل شيء ما، ولا يرتاح أبداً تقريباً.
ولكن ماذا تعني كل هذه المناطق الأرجوانية حقاً؟
كانت أداة التحليل لطيفة بما يكفي لتخبرنا أنها تسمى إعادة التدفق (reflows). إليكم تعريف ماهية إعادة التدفق من موقع مطوري Google:
إعادة التدفق (Reflow) هو اسم عملية متصفح الويب لإعادة حساب مواضع وأبعاد العناصر في المستند.. إعادة التدفق هي عملية تحظر تفاعل المستخدم (user-blocking) في المتصفح.
هذا يعني أن تحريك الفأرة في PBS يؤدي إلى تشغيل كم هائل من عمليات إعادة التدفق. والأسوأ من ذلك هو أن كل عملية إعادة تدفق توقف المتصفح مؤقتاً حتى يتمكن من إنهاء حساباته. المزيد من عمليات إعادة التدفق يعني المزيد من توقفات المتصفح المؤقتة، وتصبح التجربة أكثر تقطعاً وتلعثماً (janky). بعبارات أبسط، الكثير من المناطق الأرجوانية = سيئ.
لذا تبين أن آن كانت على حق: لدينا بالفعل اختناق كبير في الأداء، لذلك باشر فريقنا العمل على الفور لإصلاحه.
حل مشكلة إعادة التدفق / التقطع لدينا
قبل أي شيء آخر، كنا بحاجة إلى إجراء بعض الأبحاث لمعرفة السبب الحقيقي وراء حدوث إعادة التدفق هذه في المقام الأول. واكتشفنا أنه على ما يبدو، هناك الكثير من الأشياء الشائعة في Javascript التي يمكن أن تحفز المتصفح على إجراء إعادة التدفق.
تقريباً أي دالة يمكنك استخدامها للحصول على عرض عنصر DOM أو ارتفاعه أو موضعه يمكن أن تؤدي إلى تشغيل إعادة التدفق. كتب بول آيرش قائمة شاملة بجميع الخصائص والدوال في Javascript التي يمكنها فعل ذلك. الأخبار السيئة هي أن Page Builder Sandwich يستخدم الكثير من هذه الخصائص والدوال، والتي تسبب جميعها إعادة التدفق:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- والكثير غيرها…
الحل رقم 1: تخزين عمليات إعادة التدفق المكلفة في الذاكرة المؤقتة (Caching)
الحل الأكثر وضوحاً هو عدم استخدام تلك الخصائص والدوال. ومع ذلك، يعتمد PBS بشكل كبير عليها ولا يمكننا الاستغناء عنها.
الحل المتبقي لنا هو تقليص عدد الاستدعاءات لتلك الدوال قدر الإمكان. الجزء الصعب هنا هو أننا استخدمنا تلك الخصائص والدوال عدة مرات في أجزاء مختلفة من PBS، وفي معظم الأوقات، استخدمناها على نفس عنصر DOM عدة مرات. لإصلاح ذلك، أنشأنا آلية التخزين المؤقت الخاصة بنا لكل خاصية/دالة بها مشكلة.
على سبيل المثال، تعطينا دالة getBoundingClientRect قياسات البكسل الحالية لأعلى ويسار وعرض وارتفاع ويمين وأسفل عنصر DOM. نستخدم هذه الدالة طوال الوقت لحساب الموقع الذي يتم فيه رسم حدود العناصر في PBS. إليكم دالة التخزين المؤقت التي أنشأناها لهذا الغرض:
// ذاكرة التخزين المؤقت: يتم الاحتفاظ بمستطيلات العميل هنا، ويمكننا استخدام هذا لمسحها لاحقًا.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// تحقق مما إذا كنا قد حصلنا على مستطيل العميل من قبل.
if ( ! element._boundingClientRect ) {
// إذا لم يكن الأمر كذلك، فاحصل عليه ثم خزّنه للاستخدام في المستقبل.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
بدلاً من استدعاء getBoundingClientRect، أصبحنا نستخدم الآن pbsGetBoundingClientRect. والآن، في كل مرة يتم فيها الاستعلام عن مستطيل العميل، نقوم بتخزين هذه القيمة للرجوع إليها في المستقبل، مما يحد من عدد محفزات إعادة التدفق.
كنا بحاجة أيضاً إلى طريقة لمسح الذاكرة المؤقتة عندما تصبح مستطيلات العميل المخزنة غير صالحة. تتغير قيم مستطيل العميل المخزن عندما يقوم المستخدم بالتمرير في الصفحة أو إضافة/تعديل/إزالة عناصر في الشاشة. لذلك أنشأنا دالة لمسح الذاكرة المؤقتة يمكننا استدعاؤها متى احتجنا إلى قيم جديدة لمستطيل العميل:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
كما طبقنا نفس آلية التخزين المؤقت هذه لاستدعاءات الدوال المكلفة الأخرى التي نستخدمها عادةً.
الحل رقم 2: تجميع عمليات القراءة والكتابة معاً باستخدام FastDom
أدى تطبيق الحل رقم 1 إلى زيادة أداء PBS، لكنه لم يكن كافياً.
أثناء البحث في Google أكثر، عثرنا على مقال كتبه ويلسون بيج حول منع “تخبط التخطيط” (layout thrashing). يقول ويلسون:
يحدث تخبط التخطيط (إعادة التدفق) عندما تقوم JavaScript بالكتابة ثم القراءة بعنف من DOM عدة مرات مما يتسبب في إعادة تدفق المستند.
على ما يبدو، لا يكفي الحد من استدعاءاتك لخصائص ودوال محددة تسبب إعادة التدفق. فعندما تقوم بإجراء عمليات “قراءة” و”كتابة” متتالية متعددة على DOM، فإن ذلك يؤدي أيضاً إلى تشغيل إعادة تدفق المتصفح. إليكم سيناريو نموذجياً مباشرة من موقع ويلسون:
// قراءة var h1 = element1.clientHeight; // كتابة (تبطل التخطيط) element1.style.height = (h1 * 2) + 'px'; // قراءة (تطلق إعادة التدفق) var h2 = element2.clientHeight; // كتابة (تبطل التخطيط) element2.style.height = (h2 * 2) + 'px'; // قراءة (تطلق إعادة التدفق) var h3 = element3.clientHeight; // كتابة (تبطل التخطيط) element3.style.height = (h3 * 2) + 'px'; // تحدث إعادة تدفق المستند في نهاية الإطار
المثال أعلاه يؤدي إلى تشغيل 3 عمليات إعادة تدفق لأن أبعاد الصفحة بأكملها تبطل بعد عملية كتابة. الحل المقترح لهذا هو إعادة ترتيب الشيفرة البرمجية لـ تجميع كل عمليات القراءة والكتابة معاً بحيث لا يؤدي الأمر بأكمله إلا إلى تشغيل عملية إعادة تدفق واحدة للمتصفح:
// قراءة var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // كتابة (تبطل التخطيط) element1.style.height = (h1 * 2) + 'px'; element2.style.height = (h2 * 2) + 'px'; element3.style.height = (h3 * 2) + 'px'; // تحدث إعادة تدفق المستند في نهاية الإطار
المقتطف البرمجي أعلاه يجعل حل المشكلة يبدو سهلاً. ولكن في الواقع، في PBS، تحدث قراءة وكتابة الأشياء المسببة لإعادة التدفق في الكثير من الأماكن المختلفة وفي أوقات مختلفة تماماً خلال دورة حياة PBS. إعادة تنظيم آلاف الأسطر من شيفرة Javascript البرمجية لم يكن أمراً مجدياً.
لحسن الحظ، قدم ويلسون أيضاً حلاً في مقاله؛ حيث أنشأ أداة برمجية مساعدة صغيرة للمساعدة في تجميع عمليات القراءة والكتابة تلك معاً، تسمى FastDom.
إليكم بعض التعليمات البرمجية من Page Builder Sandwich التي تستخدم FastDom وتتضمن أيضاً حلنا رقم 1 أعلاه:
// تجميع عمليات القراءة من الخيوط الأخرى معًا.
fastdom.measure( function() {
// استخدام مستطيل العميل المخزن مؤقتًا لدينا.
var rect = window.pbsGetBoundingClientRect( elem );
// تجميع عمليات الكتابة من الخيوط الأخرى معًا.
fastdom.mutate( function() {
elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
elem.style.height = rect.height + 'px';
elem.style.left = rect.left + 'px';
elem.style.width = rect.width + 'px';
}.bind( this ) );
}.bind( this ) );
ما يفعله FastDom هو أنه يجمع كل استدعاءات fastdom.measure وجميع استدعاءات fastdom.mutate وينفذها بشكل غير متزامن، مما يجمع فعلياً جميع عمليات القراءة والكتابة معاً لتقليل جميع عمليات إعادة التدفق في المتصفح.
الحل رقم 3: الوصول إلى كائنات الأنماط (Style Objects) أمر مكلف
اكتشفنا أيضاً بمساعدة jsPerf أن بعض دوال جلب الأنماط كانت أسرع من غيرها.
في PBS، نحتاج عادةً إلى الحصول على القيمة الحالية لنمط معين لعنصر ما. على سبيل المثال، نستخدم هذا عادةً للحصول على الهامش العلوي:
var styles = window.getComputedStyle( elem ); var marginTop = styles.marginTop;
ولكننا اكتشفنا أن القيام بذلك أسرع بكثير:
var marginTop = elem.style.marginTop;
ومع ذلك، فإن elem.style.marginTop وgetComputedStyle مختلفان تماماً. فالأولى تعطينا التنسيق المضمن (inline styling) للعنصر فقط، ولا تأخذ في الاعتبار أي أوراق أنماط (stylesheets)؛ والثانية تأخذ في الاعتبار كلاً من الأنماط المضمنة وأوراق الأنماط.
علاوة على ذلك، حتى عندما تم تخزين نتيجة getComputedStyle مؤقتاً، كانت لا تزال بطيئة حقاً. وكما تبين، فإن الوصول إلى كائن النمط نفسه هو العملية البطيئة.
لحسن الحظ، يلحق PBS أنماطاً مضمنة بالعناصر، وبما أن الأنماط المضمنة لها الأسبقية (في معظم الأوقات) على أوراق الأنماط، فستكون هناك حالات يكون فيها الحصول على قيمة النمط المضمن كافياً. لذا، إذا جمعنا بين الطريقتين أعلاه، يمكننا التوصل إلى كود يعمل بشكل أسرع بكثير:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
نتائج تحسينات التعليمات البرمجية
استغرقت حلولنا الثلاثة المذكورة أعلاه الكثير من البحث والاختبار والتنفيذ الفعلي (وأخرت إصدار ميزة الخطوط لدينا)، ولكن في النهاية كان الأمر يستحق كل هذا العناء.
بالعودة إلى اختبارنا الأولي لتحريك الفأرة عبر الشاشة لمدة ثانيتين. إليكم المخطط الزمني القديم القبيح/المتقطع:
وإليكم كيف يبدو مخططنا الزمني الجديد بعد تحسينات الأداء:
كما ترون، هناك قدر أقل من العناصر الأرجوانية في الرسم البياني. وهناك فرق ملحوظ في كمية العمليات التي يقوم بها المتصفح. كما أن العناصر المستطيلة في الأسفل أصبحت أرق، مما يعني أن العمليات تعمل بشكل أسرع من ذي قبل.
إذا قارنت كمية المناطق الأرجوانية الطويلة/العريضة في الرسمين البيانيين القديم والجديد، فإننا نرى زيادة هائلة في الأداء بنسبة 200%! كما أننا نشهد زيادات في الأداء في جميع النواحي. ليس سيئاً على الإطلاق!
الدروس المستفادة والنتائج
لكي نكون صادقين، لم نكن نعتقد أن PBS لديه مشكلات في الأداء قبل أن تطرح آن الأمر. ويعود ذلك بشكل رئيسي إلى أننا كنا نستخدم أجهزة تركز على التطوير ولم تكن اختناقات الأداء واضحة إلى هذا الحد. وبالنظر إلى الوراء، كان ينبغي أن يكون فحص الأداء أحد أهم الأولويات. ولكن على الأقل الآن، واعتباراً من الإصدار 4.3، تمت معالجة الأمر.
بالنسبة لأولئك الذين يرغبون في تحسين أداء إضافات ووردبريس الخاصة بهم، خاصة إذا كنتم تستخدمون أيضاً Javascript بكثافة، فإليكم أهم ما تعلمته من هذه التجربة:
- قم بالحد من استدعاءاتك للخصائص/الدوال التي تؤدي إلى تشغيل إعادة التدفق أو تخزينها مؤقتاً. استخدم هذا كدليل: ما يفرض التخطيط/إعادة التدفق. القائمة الشاملة.
- قم بتجميع عمليات “القراءة” و”الكتابة” لأنماط DOM معاً. استخدم FastDom للقيام بذلك نيابة عنك.
- استخدم أداة تحليل المخطط الزمني في Chrome، وتأكد من حصولك على أقل قدر ممكن من عمليات إعادة التدفق (الأجزاء الأرجوانية).