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

مقدمة

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

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

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

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

مع ذلك، لاحظ أنه في لوحة الرسم، يوجد استثناء مهم لهذه القاعدة: إذا كانت العناصر الأساسية المتضمنة في رسم الكائن المطلوب تحتوي على مربعات حدود صغيرة (على سبيل المثال، خطوط أفقية ورأسية)، فقد يكون من الأفضل عرضها بشكلٍ منفصل (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);
}

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

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

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

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>

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

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

تجنُّب تمويه الخلفية

مثل العديد من بيئات الرسومات الأخرى، تسمح لوحة 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

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

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

تدعم لوحة 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).

لاحظ أن هذا النوع من التحسين لن يهم بعد الآن بعد أن يتم تسريع عمليات تنفيذ لوحة معالجة الرسومات، الأمر الذي سيتمكن من عرض إحداثيات لا تحتوي على أعداد صحيحة بسرعة.

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

إنّ واجهة برمجة التطبيقات requestAnimationFrame API الجديدة نسبيًا هي الطريقة المقترَحة لتنفيذ التطبيقات التفاعلية في المتصفّح. وبدلاً من أن تطلب من المتصفّح بالعرض بمعدّل ظهور ثابت معيّن، تطلب بلطف من المتصفّح استدعاء سلسلة الإجراءات الخاصة بك وسيتم استدعاؤه عندما يكون المتصفّح متاحًا. وكتأثير جانبي جميل، إذا لم تكن الصفحة في المقدّمة، يكون المتصفّح ذكيًا بما يكفي لعدم عرضها. تهدف معاودة الاتصال بـ 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، لذا يجب استخدام this shim.

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

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

الخلاصة

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

المراجع