JavaScript per character: DrawImage vs FillText

JavaScript performance comparison

Test case created by Ross Brunton

Info

ctx.fillText is slow and should be cached whenever possible. This test is an attempt to see if caching individual characters on an external canvas (or set of canvases) and using them to build a line of text offers any performance increases over drawText for the whole string. I suspect fillText with the whole string will "win", but by how much?

The tests work on a small string ("Hello World!"), and a large string (Some lorum ipsum). And consist of the following:

The reason I am doing this, as it is clear that the first option will win, is that when writing each individual character after another allows me to insert commands like "[colour red]" or "[pause 5s]" allowing a change of style, or to insert arbitrary images.

Preparation code

<canvas id="testarea" width="3500" height="20"></canvas>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

<script>
createCanvas = function(width, height) {
        var hold = document.createElement("canvas");
        hold.width = width;
        hold.height = height;
        hold.style.imageRendering = "-webkit-optimize-contrast";
        hold.getContext("2d").textBaseline = "middle";
        return hold;
};

var longStr = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc eros orci, accumsan at tempus tincidunt, faucibus sit amet erat. Etiam tempor cursus leo. Nullam non eros eu quam fermentum pharetra. Vivamus nunc purus, sollicitudin mollis vulputate non, mollis quis ante. Aliquam vitae eros eget lorem fermentum blandit. Integer dictum suscipit massa, eget viverra nisl venenatis sed. Quisque vehicula eleifend risus, vitae facilisis lacus vehicula quis. Maecenas quam elit, molestie id feugiat non, sodales mollis enim. Nullam consequat rhoncus hendrerit. Nulla facilisi. Nulla est erat, congue id lobortis at, pharetra vel risus. Donec sit amet magna lorem, semper tincidunt ligula. Mauris tempus ultricies orci sed sagittis.";

var table = {"segments":[], "segmentFree":new Uint8Array(50)};
var target = $("#testarea")[0];
target.getContext("2d").font = "14px sans";

addToTable = function(char) {
        if(char in table) return false;
       
        if(!table.segments[0]) {
                table.segments[0] = createCanvas(255, 14).getContext("2d");
                table.segments[0].font = "14px sans";
        }
       
        var width = table.segments[0].measureText(char).width;
        var seg = -1;
       
        for(var i = 0; i < table.segmentFree.length; i ++) {
                if(width + table.segmentFree[i] + 3 < 255) {
                        seg = i;
                        break;
                }
        }
       
        if(seg == -1) {
                console.warn("Out of space for character "+char+"!");
                return true;
        }
       
        if(!table.segments[seg]) {
                table.segments[seg] = createCanvas(255, 14).getContext("2d");
                table.segments[seg].font = "14px sans";
        }
       
        table[char] = new Uint8Array(3);
        table[char][0] = seg;
        table[char][1] = table.segmentFree[seg];
        table[char][2] = width;
       
        table.segmentFree[seg] += width + 3;
       
        table.segments[seg].fillText(char, table[char][1], this.padding + (this.size>>1));
};

for(var i = 0; i < "Hello World!".length; i ++) {
        addToTable("Hello World!"[i]);
}

for(var i = 0; i < longStr.length; i ++) {
        addToTable(longStr[i]);
}

function imageTextA(text) {
        var cursor = 0;
        while(text !== "") {
                var char = text[0];

                target.getContext("2d").drawImage(table.segments[table[char][0]].canvas,
                        table[char][1], 0, table[char][2], 14,
                        cursor, 0, table[char][2], 14
                );
                cursor += table[char][2];

                text = text.substr(1);
        }
}

function imageTextB(text) {
        var cursor = 0;
        for(var i = 0; i < text.length; i ++) {
                var char = text[i];

                target.getContext("2d").drawImage(table.segments[table[char][0]].canvas,
                        table[char][1], 0, table[char][2], 14,
                        cursor, 0, table[char][2], 14
                );
                cursor += table[char][2];
        }
}

function fillTextLoop(text) {
        var cursor = 0;
        for(var i = 0; i < text.length; i ++) {
                var char = text[i];

                target.getContext("2d").fillText(char, cursor, 0);
                cursor += table[char][2];
        }
}

function fillText(text) {
        target.getContext("2d").fillText(text, 0, 10);
}
</script>
<script>
Benchmark.prototype.setup = function() {
    target.getContext("2d").clearRect(0, 0, 3500, 20);
};
</script>

Preparation code output

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
Short: FillText
fillText("Hello World!");
pending…
Short: With Substr
imageTextA("Hello World!");
pending…
Short: With For Loop
imageTextB("Hello World!");
pending…
Short: FillText Loop
fillTextLoop("Hello World");
pending…
Long: FillText
fillText(longStr);
pending…
Long: With Substr
imageTextA(longStr);
pending…
Long: With For Loop
imageTextB(longStr);
pending…
Long: FillText Loop
fillTextLoop(longStr);
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment