การปรับปรุงประสิทธิภาพการทำงานของ HTML5 Canvas

บทนำ

แคนวาส HTML5 ซึ่งเริ่มต้นจากการทดลองของ Apple เป็นมาตรฐานที่รองรับมากที่สุดสำหรับกราฟิกโหมดทันที 2 มิติบนเว็บ ปัจจุบันนักพัฒนาซอฟต์แวร์จำนวนมากใช้ประโยชน์จาก โครงการมัลติมีเดีย การแสดงภาพ และเกม อย่างไรก็ตาม เมื่อแอปพลิเคชันที่เราสร้างมีความซับซ้อนมากขึ้น นักพัฒนาแอปอาจพบปัญหาด้านประสิทธิภาพโดยไม่ตั้งใจ มีแนวคิดที่ไม่เกี่ยวข้องกันมากมายเกี่ยวกับการเพิ่มประสิทธิภาพแคนวาส บทความนี้มีจุดประสงค์เพื่อรวบรวมข้อมูลบางส่วนนี้ให้เป็นแหล่งข้อมูลที่นักพัฒนาแอปเข้าใจได้ง่ายขึ้น บทความนี้ประกอบด้วยการเพิ่มประสิทธิภาพขั้นพื้นฐานที่มีผลกับสภาพแวดล้อมคอมพิวเตอร์กราฟิกทั้งหมด ตลอดจนเทคนิคเฉพาะสำหรับ Canvas ซึ่งอาจมีการเปลี่ยนแปลงเมื่อการติดตั้งใช้งาน Canvas ได้รับการปรับปรุงให้ดีขึ้น โดยเฉพาะอย่างยิ่งเมื่อผู้ให้บริการเบราว์เซอร์ใช้การเร่ง GPU ของ Canvas เทคนิคด้านประสิทธิภาพบางอย่างที่ระบุไว้อาจมีผลน้อยลง เราจะระบุข้อมูลนี้ไว้ตามความเหมาะสม โปรดทราบว่าบทความนี้ไม่ได้กล่าวถึงการใช้งาน Canvas ของ HTML5 โปรดอ่านบทความที่เกี่ยวข้องกับ Canvas เหล่านี้ใน HTML5Rocks, บทนี้ในเว็บไซต์ Dive into HTML5 หรือบทแนะนำ MDN Canvas

การทดสอบประสิทธิภาพ

ในการจัดการกับโลกแห่ง Canvas ของ HTML5 ที่เปลี่ยนแปลงอย่างรวดเร็ว การทดสอบ JSPerf (jsperf.com) จะตรวจสอบว่าการเพิ่มประสิทธิภาพที่เสนอทั้งหมดยังคงทำงานอยู่ JSPerf เป็นเว็บแอปพลิเคชันที่ช่วยให้นักพัฒนาซอฟต์แวร์เขียนการทดสอบประสิทธิภาพ JavaScript ได้ การทดสอบแต่ละรายการจะมุ่งเน้นที่ผลลัพธ์ที่คุณพยายามจะบรรลุ (เช่น การล้างแคนวาส) และมีหลายวิธีที่จะให้ผลลัพธ์เดียวกัน JSPerf จะเรียกใช้แนวทางแต่ละวิธีให้มากที่สุดเท่าที่จะเป็นไปได้ในช่วงเวลาสั้นๆ และแสดงจํานวนการทําซ้ำต่อวินาทีที่มีนัยสำคัญทางสถิติ คะแนนสูงย่อมดีกว่าเสมอ ผู้เข้าชมหน้าการทดสอบประสิทธิภาพของ JSPerf สามารถเรียกใช้การทดสอบในเบราว์เซอร์ของตนเอง และอนุญาตให้ JSPerf จัดเก็บผลการทดสอบที่ปรับมาตรฐานแล้วใน Browserscope (browserscope.org) เนื่องจากเทคนิคการเพิ่มประสิทธิภาพในบทความนี้ได้รับการสนับสนุนโดยผลลัพธ์ JSPerf คุณจึงกลับไปดูข้อมูลล่าสุดเกี่ยวกับเทคนิคนั้นว่ายังคงใช้งานได้อยู่หรือไม่ เราได้เขียนแอปพลิเคชันตัวช่วยเล็กๆ ขึ้นมาเพื่อแสดงผลลัพธ์เหล่านี้เป็นกราฟ ซึ่งฝังอยู่ในบทความนี้

ผลลัพธ์ด้านประสิทธิภาพทั้งหมดในบทความนี้จะอิงตามเวอร์ชันเบราว์เซอร์ ซึ่งกลายเป็นข้อจำกัดเนื่องจากเราไม่ทราบว่าเบราว์เซอร์ทำงานบนระบบปฏิบัติการใด หรือที่สำคัญกว่านั้นคือ HTML5 canvas ได้รับการเร่งด้วยฮาร์ดแวร์หรือไม่เมื่อทำการทดสอบประสิทธิภาพ คุณสามารถดูว่าแคนวาส HTML5 ของ Chrome มีการเร่งด้วยฮาร์ดแวร์หรือไม่โดยไปที่ about:gpu ในแถบที่อยู่

แสดงผลก่อนไปยังแคนวาสนอกหน้าจอ

หากคุณวาดรูปเรขาคณิตพื้นฐานที่คล้ายกันบนหน้าจอซ้ำหลายเฟรม ซึ่งมักเกิดขึ้นเมื่อเขียนเกม คุณจะเพิ่มประสิทธิภาพได้อย่างมากด้วยการเรนเดอร์ฉากส่วนใหญ่ล่วงหน้า การแสดงผลล่วงหน้าหมายถึงการใช้แคนวาสนอกหน้าจอ (หรือแคนวาส) แยกต่างหากในการแสดงผลรูปภาพชั่วคราว แล้วแสดงผลแคนวาสนอกหน้าจอกลับคืนมาเป็นภาพที่มองเห็นได้ ตัวอย่างเช่น สมมติว่าคุณวาดภาพมาริโอวิ่งซ้ำที่ 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;

รวมการเรียกใช้ Canvas หลายรายการเข้าด้วยกัน

เนื่องจากการวาดเป็นการดำเนินการที่มีค่าใช้จ่ายสูง การโหลดเครื่องสถานะการวาดด้วยชุดคำสั่งที่ยาวๆ แล้วให้ทิ้งชุดคำสั่งทั้งหมดลงในบัฟเฟอร์วิดีโอจึงมีประสิทธิภาพมากกว่า

ตัวอย่างเช่น เมื่อวาดเส้นหลายเส้น การสร้างเส้นทางเดียวที่มีเส้นทั้งหมดอยู่ในนั้นจะมีประสิทธิภาพมากกว่า และวาดด้วยการวาดเส้นเดียว กล่าวคือ ให้ทำดังนี้แทนการวาดเส้นแยกกัน

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)

หลีกเลี่ยงการเปลี่ยนแปลงสถานะ Canvas ที่ไม่จำเป็น

องค์ประกอบ Canvas ของ 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

ใช้ผืนผ้าใบหลายเลเยอร์สำหรับฉากที่ซับซ้อน

ดังที่กล่าวไปก่อนหน้านี้ การวาดภาพขนาดใหญ่มีค่าใช้จ่ายสูงและควรหลีกเลี่ยงหากเป็นไปได้ นอกจากการใช้ผืนผ้าใบอื่นสำหรับการเรนเดอร์นอกหน้าจอตามที่แสดงในส่วนการเรนเดอร์ล่วงหน้าแล้ว เรายังใช้ผืนผ้าใบที่วางซ้อนกันได้ด้วย ด้วยการใช้ความโปร่งใสในผืนผ้าใบพื้นหน้า เราสามารถใช้ GPU ในการประกอบอัลฟ่าเข้าด้วยกันในเวลาแสดงผล คุณอาจตั้งค่าดังนี้โดยมีภาพพิมพ์แคนวาส 2 ภาพที่วางซ้อนกัน

<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>

ข้อดีของการใช้ภาพพิมพ์แคนวาส 2 ภาพนี้ก็คือเมื่อเราวาดหรือล้างภาพพิมพ์แคนวาสที่อยู่เบื้องหน้า เราจะไม่แก้ไขพื้นหลัง หากเกมหรือแอปมัลติมีเดียของคุณแยกออกเป็นพื้นหน้าและพื้นหลังได้ ให้ลองแสดงผลบนผืนผ้าใบแยกกันเพื่อเพิ่มประสิทธิภาพอย่างเห็นได้ชัด

คุณมักจะใช้ประโยชน์จากการรับรู้ที่ไม่สมบูรณ์ของมนุษย์และแสดงผลเบื้องหลังเพียงครั้งเดียวหรือช้ากว่าเบื้องหน้า (ซึ่งมักจะดึงดูดความสนใจของผู้ใช้ส่วนใหญ่) เช่น คุณอาจเรนเดอร์พื้นหน้าทุกครั้งที่เรนเดอร์ แต่เรนเดอร์พื้นหลังทุกๆ N เฟรมเท่านั้น และโปรดทราบว่าวิธีนี้เหมาะกับแคนวาสแบบผสมจำนวนเท่าใดก็ได้ หากแอปพลิเคชันทำงานกับโครงสร้างแบบนี้ได้ดีกว่า

หลีกเลี่ยงการใช้ 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);

ทำความรู้จักวิธีต่างๆ ในการล้างภาพพิมพ์แคนวาส

เนื่องจาก Canvas ของ HTML5 เป็นรูปแบบการวาดโหมดทันที จึงต้องมีการวาดฉากใหม่อย่างชัดเจนในแต่ละเฟรม ด้วยเหตุนี้ การล้าง Canvas จึงเป็นการดำเนินการที่สำคัญโดยพื้นฐานสำหรับแอปและเกม Canvas แบบ HTML5 ตามที่ระบุไว้ในส่วนหลีกเลี่ยงการเปลี่ยนแปลงสถานะ Canvas นั้น การล้าง Canvas ทั้งหมดมักเป็นสิ่งที่ไม่ต้องการ แต่หากคุณต้องทํา ก็มี 2 ตัวเลือก ได้แก่ การเรียกใช้ context.clearRect(0, 0, width, height) หรือการใช้แฮ็กเฉพาะ Canvas เพื่อทำดังนี้ canvas.width = canvas.width; ณ เวลาที่เขียน โดยทั่วไป clearRect จะมีประสิทธิภาพดีกว่าเวอร์ชันที่รีเซ็ตความกว้าง แต่บางกรณีการใช้แฮ็กการรีเซ็ต canvas.width จะเร็วกว่ามากใน Chrome 14

โปรดใช้เคล็ดลับนี้อย่างระมัดระวัง เนื่องจากขึ้นอยู่กับการใช้งาน Canvas พื้นฐานเป็นอย่างมากและอาจมีการเปลี่ยนแปลงได้ ดูข้อมูลเพิ่มเติมได้ที่บทความของ Simon Sarris เกี่ยวกับการล้างภาพพิมพ์แคนวาส

หลีกเลี่ยงพิกัดทศนิยม

แคนวาส HTML5 รองรับการแสดงผลระดับพิกเซลย่อย และปิดการแสดงผลระดับพิกเซลย่อยไม่ได้ หากคุณวาดด้วยพิกัดที่ไม่ใช่จำนวนเต็ม แคนวาสจะใช้การลบรอยหยักโดยอัตโนมัติเพื่อพยายามทำให้เส้นเรียบ เอฟเฟกต์ภาพนี้นำมาจากบทความเกี่ยวกับประสิทธิภาพของ Canvas ระดับพิกเซลย่อยโดย Seb Lee-Delisle

พิกเซลย่อย

หากสไปรท์ที่ผ่านการปรับเรียบไม่ใช่เอฟเฟ็กต์ที่คุณต้องการ การแปลงพิกัดเป็นจำนวนเต็มโดยใช้ 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 มีการเร่งโดยใช้ GPU ซึ่งจะทำให้แสดงผลพิกัดที่ไม่ใช่จำนวนเต็มได้อย่างรวดเร็ว

เพิ่มประสิทธิภาพภาพเคลื่อนไหวด้วย requestAnimationFrame

requestAnimationFrame API ซึ่งเป็น 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 นี้มีผลกับ Canvas รวมถึงเทคโนโลยีการแสดงผลอื่นๆ เช่น WebGL ในขณะที่เขียน API นี้มีให้บริการเฉพาะใน Chrome, Safari และ Firefox เท่านั้น คุณจึงควรใช้ shim นี้

การใช้งานแคนวาสบนอุปกรณ์เคลื่อนที่ส่วนใหญ่ทำงานช้า

มาพูดถึงอุปกรณ์เคลื่อนที่กัน ขออภัย ณ เวลาที่เขียน มีเพียง iOS 5.0 เบต้าที่ใช้ Safari 5.1 เท่านั้นที่มีการใช้งาน Canvas บนอุปกรณ์เคลื่อนที่ที่เร่งด้วย GPU หากไม่มีการเร่งด้วย GPU โดยทั่วไปเบราว์เซอร์ในอุปกรณ์เคลื่อนที่จะมี CPU ที่ไม่มีประสิทธิภาพเพียงพอสำหรับแอปพลิเคชันสมัยใหม่ที่ใช้ Canvas การทดสอบ JSPerf จํานวนหนึ่งตามที่อธิบายไว้ข้างต้นมีประสิทธิภาพแย่กว่าในอุปกรณ์เคลื่อนที่เมื่อเทียบกับเดสก์ท็อปอย่างมาก ซึ่งจํากัดประเภทของแอปข้ามอุปกรณ์ที่คาดว่าจะทํางานได้สําเร็จ

บทสรุป

โดยสรุปแล้ว บทความนี้ครอบคลุมเทคนิคการเพิ่มประสิทธิภาพที่มีประโยชน์อย่างครอบคลุม ซึ่งจะช่วยให้คุณพัฒนาโปรเจ็กต์แคนวาส HTML5 ที่มีประสิทธิภาพ เมื่อได้เรียนรู้สิ่งใหม่ๆ แล้ว ก็ไปเพิ่มประสิทธิภาพให้กับผลงานสุดเจ๋งของคุณได้เลย หรือหากตอนนี้คุณยังไม่มีเกมหรือแอปพลิเคชันที่จะเพิ่มประสิทธิภาพ ให้ดูแรงบันดาลใจจาก Chrome Experiments และ Creative JS

ข้อมูลอ้างอิง