HTML5 कैनवस की परफ़ॉर्मेंस को बेहतर बनाना

परिचय

HTML5 कैनवस, Apple के प्रयोग के तौर पर शुरू किया गया था. यह वेब पर 2D इमीडिएट मोड ग्राफ़िक के लिए सबसे ज़्यादा इस्तेमाल किया जा सकने वाला स्टैंडर्ड है. अब कई डेवलपर, कई तरह के मल्टीमीडिया प्रोजेक्ट, विज़ुअलाइज़ेशन, और गेम के लिए इस पर भरोसा करते हैं. हालांकि, जैसे-जैसे हमारे बनाए जाने वाले ऐप्लिकेशन मुश्किल होते जाते हैं, डेवलपर अनजाने में परफ़ॉर्मेंस की समस्या का सामना करते हैं. कैनवस की परफ़ॉर्मेंस को ऑप्टिमाइज़ करने के बारे में कई बातें हैं. इस लेख का मकसद, डेवलपर के लिए इस जानकारी को एक ऐसे संसाधन में इकट्ठा करना है जिसे आसानी से समझा जा सके. इस लेख में, सभी कंप्यूटर ग्राफ़िक्स एनवायरमेंट पर लागू होने वाले बुनियादी ऑप्टिमाइज़ेशन के साथ-साथ कैनवस के हिसाब से बनाई गई तकनीकें शामिल हैं. कैनवस के बेहतर तरीके से लागू होने पर, इन तकनीकों में बदलाव हो सकता है. खास तौर पर, जब ब्राउज़र के वेंडर कैनवस जीपीयू ऐक्सेलरेशन को लागू करेंगे, तो परफ़ॉर्मेंस को बेहतर बनाने के लिए बताई गई कुछ तकनीकों का असर कम हो सकता है. ज़रूरत पड़ने पर इसे नोट किया जाएगा. ध्यान दें कि इस लेख में, HTML5 कैनवस के इस्तेमाल के बारे में नहीं बताया गया है. इसके लिए, HTML5Rocks पर कैनवस से जुड़े ये लेख पढ़ें. इसके अलावा, 'एचटीएमएल5 में दिलचस्पी रखें' साइट पर मौजूद यह चैप्टर या MDN Canvas ट्यूटोरियल देखें.

परफ़ॉर्मेंस की जांच

HTML5 कैनवस की तेज़ी से बदलती दुनिया को ध्यान में रखते हुए, JSPerf (jsperf.com) टेस्ट की मदद से यह पुष्टि की जाती है कि सुझाया गया हर ऑप्टिमाइज़ेशन अब भी काम करता है. बनाएँ एक वेब ऐप्लिकेशन है जो डेवलपर को JavaScript प्रदर्शन परीक्षण लिखने देता है. हर टेस्ट, उस नतीजे पर फ़ोकस करता है जिसे आपको हासिल करना है. उदाहरण के लिए, कैनवस को खाली करना. साथ ही, इसमें एक जैसे नतीजे पाने के लिए कई तरीके शामिल होते हैं. JSPerf, कम समयावधि में हर तरीके को ज़्यादा से ज़्यादा बार चलाता है. साथ ही, हर सेकंड में आइटरेशन की आंकड़ों के हिसाब से अहम संख्या देता है. ज़्यादा स्कोर हमेशा बेहतर होते हैं! JSPerf पर परफ़ॉर्मेंस टेस्ट पेज पर आने वाले लोग, अपने ब्राउज़र पर टेस्ट चला सकते हैं. साथ ही, JSPerf को Browserscope (browserscope.org) पर, नॉर्मलाइज़ किए गए टेस्ट के नतीजे सेव करने की अनुमति दे सकते हैं. इस लेख में दी गई ऑप्टिमाइज़ेशन तकनीकों का बैक अप, जेएसपीरएफ़ नतीजे से लिया गया है. इसलिए, आपके पास इस बात की अप-टू-डेट जानकारी देखने का विकल्प है कि यह तकनीक अब भी लागू है या नहीं. मैंने एक छोटा हेल्पर ऐप्लिकेशन लिखा है, जो इन नतीजों को ग्राफ़ के तौर पर रेंडर करता है. इन ग्राफ़ को इस लेख में एम्बेड किया गया है.

इस लेख में दिए गए सभी परफ़ॉर्मेंस के नतीजे, ब्राउज़र वर्शन के हिसाब से तय होते हैं. यह एक समस्या है, क्योंकि हमें यह नहीं पता कि ब्राउज़र किस ऑपरेटिंग सिस्टम पर चल रहा था. इससे भी ज़्यादा अहम बात यह है कि परफ़ॉर्मेंस की जांच के दौरान, HTML5 कैनवस को हार्डवेयर से तेज़ किया गया था या नहीं. पता बार में about:gpu पर जाकर, यह पता लगाया जा सकता है कि Chrome के HTML5 कैनवस में हार्डवेयर से तेज़ी लाने की सुविधा चालू है या नहीं.

ऑफ़-स्क्रीन कैनवस पर पहले से रेंडर करना

अगर स्क्रीन पर कई फ़्रेम में, मिलते-जुलते प्रिमिटिव को फिर से ड्रॉ किया जा रहा है, तो सीन के बड़े हिस्से को पहले से रेंडर करके, गेम की परफ़ॉर्मेंस में सुधार किया जा सकता है. गेम लिखते समय ऐसा अक्सर होता है. पहले से रेंडर करने का मतलब है कि किसी अलग ऑफ़-स्क्रीन कैनवस (या कैनवस) का इस्तेमाल करके, कुछ समय के लिए दिखने वाली इमेज को रेंडर करना. इसके बाद, ऑफ़-स्क्रीन कैनवस को फिर से दिखने वाले कैनवस पर रेंडर करना. उदाहरण के लिए, मान लें कि आपको हर सेकंड 60 फ़्रेम में, मैरिओ को दौड़ते हुए फिर से दिखाना है. आप या तो हर फ्रेम पर उसकी टोपी, मूंछ, और “M” को फिर से बना सकते हैं या ऐनिमेशन चलाने से पहले Mario गेम को प्री-रेंडर कर सकते हैं. पेज को पहले से रेंडर न किया गया हो:

// 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 फ़्रेम के लिए बैकग्राउंड रेंडर किया जा सकता है. यह भी ध्यान रखें कि अगर आपका ऐप्लिकेशन इस तरह के स्ट्रक्चर के साथ बेहतर तरीके से काम करता है, तो यह तरीका किसी भी संख्या के कॉम्पोज़िट कैनवस के लिए अच्छा साबित होता है.

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 आम तौर पर चौड़ाई को रीसेट करने वाले वर्शन से बेहतर परफ़ॉर्म करता है. हालांकि, कुछ मामलों में Chrome 14 में canvas.width रीसेट करने वाले हैक का इस्तेमाल करना काफ़ी तेज़ होता है

इस सलाह का इस्तेमाल करते समय सावधानी बरतें, क्योंकि यह कैनवस के लागू होने के तरीके पर काफ़ी निर्भर करता है. साथ ही, इसमें बदलाव भी हो सकता है. ज़्यादा जानकारी के लिए, कैनवस मिटाने के बारे में साइमन सरीस का लेख पढ़ें.

फ़्लोटिंग पॉइंट कोऑर्डिनेट से बचें

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 FPS कॉलबैक रेट है, लेकिन इसकी कोई गारंटी नहीं है. इसलिए, आपको यह ट्रैक करना होगा कि आखिरी बार रेंडर करने के बाद कितना समय बीत चुका है. यह कुछ ऐसा दिख सकता है:

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 में उपलब्ध है, इसलिए आपको इस शिम का इस्तेमाल करना चाहिए.

ज़्यादातर मोबाइल कैनवस को लागू करने में ज़्यादा समय लगता है

आइए, मोबाइल के बारे में बात करते हैं. फ़िलहाल, Safari 5.1 के साथ काम करने वाले iOS 5.0 बीटा वर्शन पर ही, जीपीयू की मदद से तेज़ी से काम करने वाले मोबाइल कैनवस को लागू किया जा सकता है. जीपीयू ऐक्सेलरेशन के बिना, मोबाइल ब्राउज़र में आम तौर पर कैनवस पर काम करने वाले आधुनिक ऐप्लिकेशन के लिए, ज़रूरत के मुताबिक सीपीयू नहीं होते. ऊपर बताए गए JSPerf टेस्ट में से कई, डेस्कटॉप की तुलना में मोबाइल पर काफ़ी खराब परफ़ॉर्म करते हैं. इससे, उन क्रॉस-डिवाइस ऐप्लिकेशन पर काफ़ी पाबंदी लगती है जो आसानी से काम कर सकते हैं.

नतीजा

रीकैप करने के लिए, इस लेख में ऑप्टिमाइज़ेशन की काम की तकनीकों के बारे में बताया गया है. इनकी मदद से, बेहतर परफ़ॉर्म करने वाले HTML5 कैनवस-आधारित प्रोजेक्ट बनाए जा सकते हैं. अब आपने यहां कुछ नया सीख लिया है, तो अपने शानदार क्रिएशन को ऑप्टिमाइज़ करें. इसके अलावा, अगर आपके पास फ़िलहाल ऐसा कोई गेम या ऐप्लिकेशन नहीं है जिसे ऑप्टिमाइज़ करना है, तो Chrome एक्सपेरिमेंट और Creative JS देखें.

रेफ़रंस