تحسين أداء Canvas في HTML5

مقدمة

لوحة HTML5، التي بدأت كتجربة من Apple، هي الإطار الأكثر استخدامًا على نطاق واسع لرسومات الوضع الفوري ثنائية الأبعاد على الويب. يعتمد العديد من المطورين عليه الآن في مجموعة متنوعة من مشروعات الوسائط المتعددة والتصورات والألعاب. ومع ذلك، مع زيادة تعقيد التطبيقات التي ننشئها، يواجه المطوّرون بدون قصد قيودًا على الأداء. هناك الكثير من المعلومات غير المترابطة حول تحسين أداء لوحة الالتقاط. تهدف هذه المقالة إلى دمج بعض هذه النصوص في مرجع يسهل فهمه للمطوّرين. تتضمّن هذه المقالة تحسينات أساسية تنطبق على جميع بيئات رسومات الكمبيوتر، بالإضافة إلى أساليب خاصة باللوحة يمكن أن تتغيّر مع تحسين عمليات تنفيذ اللوحة. وعلى وجه الخصوص، عندما يستخدم مورّدو المتصفِّحات تسريع وحدة معالجة الرسومات للوحة الرسم، قد تصبح بعض أساليب الأداء الموضّحة والتي تمت مناقشتها أقل تأثيرًا. سيتم الإشارة إلى ذلك حيثما ينطبق. يُرجى العِلم أنّ هذه المقالة لا تتناول استخدام لوحة HTML5. لهذا الغرض، اطّلِع على المقالات المتعلّقة باللوحة على HTML5Rocks، وهذا الفصل على موقع Dive into HTML5 الإلكتروني أو الدليل التعليمي MDN Canvas.

اختبار الأداء

للتعامل مع التغييرات السريعة في لوحة HTML5، تتحقّق اختبارات JSPerf (jsperf.com) من أنّ كل تحسين مقترَح لا يزال يعمل. JSPerf هو تطبيق ويب يتيح للمطوّرين كتابة اختبارات أداء JavaScript. يركز كل اختبار على نتيجة تحاول تحقيقها (على سبيل المثال، محو اللوحة)، ويضم طرقًا متعددة لتحقيق النتيجة نفسها. يُجري JSPerf كل نهج بقدر الإمكان خلال فترة زمنية قصيرة ويمنح عددًا ذا دلالة إحصائية من التكرارات في الثانية. النتائج الأعلى دائمًا ما تكون أفضل! يمكن للزوّار الذين ينتقلون إلى صفحة اختبار الأداء في JSPerf إجراء الاختبار على المتصفّح، والسماح لخدمة JSPerf بتخزين نتائج الاختبار المعدَّلة على Browserscope (browserscope.org). بما أنّ أساليب التحسين الواردة في هذه المقالة مدعومة بنتيجة JSPerf، يمكنك الرجوع للاطّلاع على معلومات محدّثة حول ما إذا كانت التقنية لا تزال سارية أم لا. لقد كتبتُ تطبيقًا مساعدًا صغيرًا يعرض هذه النتائج في شكل رسوم بيانية، وتم تضمين هذا التطبيق في هذه المقالة.

تستند جميع نتائج الأداء الواردة في هذه المقالة إلى إصدار المتصفّح. يُعدّ ذلك عائقًا، لأنّنا لا نعرف نظام التشغيل الذي كان المتصفّح يعمل عليه، أو ما هو أكثر أهمية، ما إذا كان قد تم تسريع canvas HTML5 باستخدام الأجهزة عند إجراء اختبار الأداء. يمكنك معرفة ما إذا كان يتم تسريع لوحة HTML5 في Chrome من خلال الانتقال إلى about:gpu في شريط العناوين.

التقديم المُسبَق على لوحة خارج الشاشة

إذا كنت تعيد رسم عناصر أساسية مشابهة على الشاشة في عدّة لقطات، كما هو الحال غالبًا عند كتابة لعبة، يمكنك تحقيق تحسينات كبيرة في الأداء من خلال التقديم المسبق لأجزاء كبيرة من المشهد. ويعني التقديم المُسبَق استخدام لوحات منفصلة (أو لوحات) خارج الشاشة لعرض الصور المؤقتة، ثم عرض اللوحات خارج الشاشة على الشاشة المرئية. على سبيل المثال، لنفترض أنّك تعيد رسم شخصية "ماريو" وهي تركض بمعدّل 60 لقطة في الثانية. يمكنك إعادة رسم قبّعته وشاربه والحرف "M" في كل إطار، أو يمكنك تقديم عرض مسبق لشخصية ماريو قبل تشغيل الصورة المتحركة. بدون معالجة مسبقة:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

العرض المُسبَق:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

ولاحِظ استخدام requestAnimationFrame، الذي سنناقشه بمزيد من التفصيل في قسم لاحق.

يكون هذا الأسلوب فعّالًا بشكل خاص عندما تكون عملية التقديم (drawMario في المثال أعلاه) باهظة التكلفة. وخير مثال على ذلك هو عرض النص، وهي عملية مكلفة للغاية.

ومع ذلك، يُظهر الأداء السيئ لحالة الاختبار "ملفات تمّ عرضها مسبقًا بشكل غير محكم". عند المعالجة المسبقة، من المهم التأكّد من أنّ اللوحة المؤقتة تلائم الصورة التي ترسمها بشكلٍ محكم، وإلا سيتأثر الأداء الذي تحقّقه المعالجة المسبقة خارج الشاشة بانخفاض الأداء الناتج عن نسخ لوحة كبيرة إلى لوحة أخرى (يختلف ذلك حسب حجم الهدف المصدر). لوحة مريحة في الاختبار أعلاه أصغر ببساطة:

can2.width = 100;
can2.height = 40;

مقارنةً بالطريقة الفضفاضة التي تؤدي إلى أداء أسوأ:

can3.width = 300;
can3.height = 100;

تجميع مكالمات لوحات العرض معًا

بما أنّ عملية الرسم تتطلّب الكثير من الموارد، من الأفضل تحميل آلة حالة الرسم بمجموعة طويلة من الأوامر، ثم تفريغ كلّها في ذاكرة التخزين المؤقت للفيديو.

على سبيل المثال، عند رسم خطوط متعددة، من الأفضل إنشاء مسار واحد يحتوي على جميع الخطوط ورسمه من خلال طلب رسم واحد. بعبارة أخرى، بدلاً من رسم خطوط منفصلة:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

نحصل على أداء أفضل من خلال رسم خط متعدّد الأضلاع واحد:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

ينطبق ذلك أيضًا على عالم لوحة HTML5. عند رسم مسار معقد، على سبيل المثال، من الأفضل وضع جميع النقاط في المسار بدلاً من عرض الأجزاء بشكل منفصل (jsperf).

تجدر الإشارة إلى أنّ هناك استثناءً مهمًا لهذا القاعدة في Canvas: إذا كانت العناصر الأساسية المُستخدَمة في رسم الجسم المطلوب تحتوي على حدود مربّعة صغيرة (مثل الخطوط الأفقية والرأسية)، قد يكون أكثر فعالية عرضها بشكل منفصل (jsperf).

تجنَّب تغييرات حالة اللوحة غير الضرورية.

يتم تنفيذ عنصر لوحة HTML5 على آلة حالة تتبع عناصر مثل أنماط الملء والخطوط، بالإضافة إلى النقاط السابقة التي تشكّل المسار الحالي. عند محاولة تحسين أداء الرسومات، يكون من المغري التركيز فقط على عرض الرسومات. ومع ذلك، يمكن أن يؤدي التلاعب بآلة الحالة أيضًا إلى زيادة تكلفة الأداء. على سبيل المثال، إذا كنت تستخدم ألوان ملء متعددة لعرض مشهد، يكون الأداء أرخص عند العرض حسب اللون بدلاً من حسب موضع العنصر على لوحة الرسم. لمحاولة معالجة نمط خطوط رفيعة، يمكنك معالجة شريط وتغيير الألوان ومعالجة الشريط التالي وما إلى ذلك:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

أو عرض جميع الشرائط الفردية ثم كل الشرائط الزوجية:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

وكما هو متوقع، تكون النهج المتداخل أبطأ لأن تغيير جهاز الحالة مكلف.

عرض الاختلافات في الشاشة فقط، وليس الحالة الجديدة بالكامل

وكما هو متوقع، يكون عرض محتوى أقل على الشاشة أقل تكلفة من عرض المحتوى بوتيرة أكبر. إذا كانت لديك اختلافات متزايدة فقط بين redraws، يمكنك تحسين الأداء بشكل كبير من خلال رسم اختلاف بسيط. بعبارة أخرى، بدلاً من محو الشاشة بأكملها قبل الرسم:

context.fillRect(0, 0, canvas.width, canvas.height);

تتبَّع مربّع الحدود الذي تم رسمه، ولا تمحو سوى هذا المربّع.

context.fillRect(last.x, last.y, last.width, last.height);

إذا كنت معتادًا على رسومات الكمبيوتر، قد تعرف أيضًا هذه التقنية باسم "إعادة رسم المناطق"، حيث يتم حفظ مربع الإحاطة المعروض سابقًا، ثم يتم محوه عند كل عرض. تنطبق هذه التقنية أيضًا على سياقات العرض المستندة إلى وحدات البكسل، كما هو موضّح في مقالة محاكاة لمحاكي جهاز Nintendo في JavaScript.

استخدام لوحات متعددة الطبقات للمشاهد المعقّدة

كما ذكرنا سابقًا، إنّ رسم الصور الكبيرة باهظ التكلفة ويجب تجنُّبه إن أمكن. بالإضافة إلى استخدام لوحة أخرى للعرض خارج الشاشة، كما هو موضّح في قسم "العرض المُسبَق"، يمكننا أيضًا استخدام لوحات رسومات مطبّقة فوق بعضها. باستخدام الشفافية في لوحة العناصر الأمامية، يمكننا الاعتماد على وحدة معالجة الرسومات لدمج قيم شفافية العناصر معًا في وقت العرض. يمكنك إعداد ذلك على النحو التالي، باستخدام لوحتَين موضعتَين بدقة فوق بعضهما.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

تتمثل ميزة استخدام لوحة واحدة فقط في أنّه عند رسم لوحة المقدّمة أو محو محتواها، لا نُعدّل الخلفية أبدًا. إذا كان من الممكن تقسيم اللعبة أو تطبيق الوسائط المتعددة إلى مقدمة وخلفية، ننصحك بعرضها على لوحات منفصلة للحصول على تحسين كبير في الأداء.

يمكنك غالبًا الاستفادة من التصور البشري غير الكامل وعرض الخلفية مرة واحدة فقط أو بسرعة أبطأ مقارنةً بالمقدّمة (التي من المرجّح أن تشغَل معظم انتباه المستخدم). على سبيل المثال، يمكنك عرض المقدّمة في كلّ مرّة من مرّات العرض، ولكن يمكنك عرض الخلفية كلّ إطار بعد عدد معيّن من اللقطات. يُرجى العلم أيضًا أنّ هذا الأسلوب يُطبَّق بشكل عام على أي عدد من لوحات الالتقاط المركبة إذا كان تطبيقك يعمل بشكل أفضل مع هذا النوع من البنية.

تجنَّب استخدام shadowBlur.

مثل العديد من بيئات الرسومات الأخرى، تسمح لوحة HTML5 للمطوّرين ب تمويه العناصر الأساسية، ولكن يمكن أن تكون هذه العملية باهظة التكلفة:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

التعرّف على الطرق المختلفة لمحو اللوحة

بما أنّ لوحة HTML5 هي نموذج رسم الوضع الفوري، يجب إعادة رسم المشهد بشكل صريح في كل إطار. لهذا السبب، فإنّ محو اللوحة هو عملية مهمة بشكل أساسي لتطبيقات HTML5 اللوحة وألعابها. كما ذُكر في قسم تجنُّب تغييرات حالة لوحة الرسم، غالبًا ما يكون محو لوحة الرسم بالكامل غير محبّذ، ولكن إذا كان يجب إجراء ذلك، يتوفّر خياران: الاتصال بـ context.clearRect(0, 0, width, height) أو استخدام اختراق خاص بلوحة الرسم لإجرائه: canvas.width = canvas.width. في وقت الكتابة، يتفوّق clearRect بشكل عام على إصدار إعادة ضبط العرض، ولكن في بعض الحالات يكون استخدام canvas.width في Chrome 14 أسرع في تنفيذ ذلك.

يُرجى توخّي الحذر عند استخدام هذه النصيحة، لأنّها تعتمد بشكل كبير على تنفيذ canvas الأساسي وقابلة للتغيير بشكل كبير. لمزيد من المعلومات، يمكنك الاطّلاع على مقالة "سايمون ساريس" حول محو اللوحة.

تجنُّب استخدام إحداثيات النقطة العائمة

تتيح لوحة HTML5 عرض العناصر بدقة أقل من وحدات البكسل، ولا يمكن إيقاف هذه الميزة. وإذا كنت ترسم باستخدام إحداثيات ليست أعدادًا صحيحة، يتم استخدام ميزة تمويه الحواف تلقائيًا لمحاولة تنعيم الخطوط. في ما يلي التأثير المرئي، مأخوذ من هذه المقالة حول أداء لوحة الألوان الفرعية البكسل من تأليف "سيب لي-ديليسلي":

وحدات البكسل الفرعية

إذا لم يكن المؤثر المُنعمَد هو التأثير الذي تبحث عنه، يمكن أن يكون أسرع بكثير لتحويل إحداثياتك إلى أعداد صحيحة باستخدام Math.floor أو Math.round (jsperf):

لتحويل إحداثيات النقطة العائمة إلى أعداد صحيحة، يمكنك استخدام عدة أساليب ذكية، وأكثرها فعالية هي إضافة نصف إلى الرقم المستهدَف، ثم إجراء عمليات على البتات في النتيجة لإزالة الجزء الكسري.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

يمكنك الاطّلاع على تفاصيل الأداء الكاملة هنا (jsperf).

تجدر الإشارة إلى أنّ هذا النوع من التحسين لن يُعدّ مهمًا بعد أن يتم تسريع تنفيذات canvas باستخدام وحدة معالجة الرسومات، ما سيتيح عرض إحداثيات غير صحيحة بسرعة.

تحسين الصور المتحركة باستخدام requestAnimationFrame

إنّ واجهة برمجة تطبيقات requestAnimationFrame الجديدة نسبيًا هي الطريقة المقترَحة لتنفيذ التطبيقات التفاعلية في المتصفّح. بدلاً من طلب من المتصفّح عرض المحتوى بمعدّل مرّات محدد، يمكنك طلب من المتصفّح بشكل مهذَّب استدعاء روتين العرض والاتّصال بك عندما يكون المتصفّح متاحًا. كنتيجة جانبية إيجابية، إذا لم تكن الصفحة في المقدّمة، يكون المتصفّح ذكيًا بما يكفي لعدم عرضها. تهدف عملية معاودة الاتصال requestAnimationFrame إلى الحصول على معدّل معاودة الاتصال يبلغ 60 لقطة في الثانية، ولكن ذلك لا يضمن ذلك، لذا عليك تتبُّع المدّة الزمنية منذ آخر عرض. يمكن أن يبدو هذا على النحو التالي:

var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
  var delta = Date.now() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

يُرجى العِلم أنّ استخدام requestAnimationFrame ينطبق على لوحات الرسم بالإضافة إلى تقنيات العرض الأخرى مثل WebGL. في وقت كتابة هذه المقالة، لا تتوفّر واجهة برمجة التطبيقات هذه إلا في Chrome وSafari و Firefox، لذا عليك استخدام هذا العنصر الوسيط.

معظم عمليات تنفيذ لوحة الرسم على الأجهزة الجوّالة بطيئة

لنتحدّث عن الأجهزة الجوّالة. في وقت كتابة الردّ، كان الإصدار التجريبي من نظام التشغيل iOS 5.0 والذي يعمل بالإصدار Safari 5.1 فقط هو التطبيق الذي تم تسريعه من خلال وحدة معالجة الرسومات على الأجهزة الجوّالة. بدون تسريع وحدة معالجة الرسومات، لا تتضمّن متصفحات الأجهزة الجوّالة عمومًا معالجات مركزية قوية بما يكفي لتشغيل التطبيقات الحديثة المستندة إلى الشاشة الكاملة. يُظهر عدد من اختبارات JSPerf الموضّحة أعلاه أداءً أسوأ بكثير على الأجهزة الجوّالة مقارنةً بأجهزة الكمبيوتر المكتبي، ما يؤدي إلى محدودية كبيرة في أنواع التطبيقات المتوافقة مع جميع الأجهزة التي يمكنك توقّع تشغيلها بنجاح.

الخاتمة

للتلخيص، تناولت هذه المقالة مجموعة شاملة من أساليب التحسين المفيدة التي ستساعدك على تطوير مشروعات عالية الأداء تعتمد على لوحة HTML5. الآن بعد أن تعلمت شيئًا جديدًا هنا، تقدم وحسِّن إبداعاتك الرائعة. وإذا لم يكن لديك حاليًا لعبة أو تطبيقًا لتحسينهما، يمكنك الاطّلاع على تجارب Chrome وCreative JS للحصول على أفكار.

المراجع