JSFiddle - React, Tailwind, and code Playground

HTML

<canvas id="canvas1" width="500" height="500"></canvas>

JavaScript

var can = document.getElementById('canvas1');
var ctx = can.getContext('2d');

var fontCache = '';

function VerticalPanel() {
     this.children = [];   
}

VerticalPanel.prototype.draw = function(x,y) {
    // clip the panel's space (useless clip for sake of example)
    ctx.save();
    ctx.beginPath();
    ctx.rect(0,0,500,500);
    ctx.clip();
    
    // Draw panel's children
    var l = this.children.length;
    for (var i = 0; i < l; i++) {
        var child = this.children[i];
        child.draw(x,y);
    }
    
    ctx.restore(); // this restore is going to stop any text objects in the next panel from taking advantage of the cache because it resets ctx.font. But it is needed because there is no other way to clear clipping regions.
}

function TextChild(text) {
     this.text = 'default';
     this.font = '32px sans-serif';
     if (text) this.text = text;
}
    
TextChild.prototype.draw = function(x,y) {
    // set font if different from cache
    if (fontCache !== this.font) {
        ctx.font = this.font; // it would be nice if this only happened once
        fontCache = this.font;
    }

    
    // another way of looking at the same problem is to cache this way:
    /*
    if (ctx.font !== this.font) {
        ctx.font = this.font; // it would be nice if this only happened once
        console.log('occured'); // but happens 2 times
    }
    */
    
    
    // clip the font's space (useless clip for sake of example)
    // This clip does not interfere with the problem discussed.
    ctx.save();
    ctx.beginPath();
    ctx.rect(0,0,500,500);
    ctx.clip();
    
    ctx.fillText(this.text, x, y);
    
    ctx.restore();
}

// Draw two nested objects that should have the same font but do not

var p = new VerticalPanel();
p.children = [new TextChild('one')];

var p2 = new VerticalPanel();
p2.children = [new TextChild('two')];

p.draw(50,50);
p2.draw(150,50);