Render the first paint on the server and every update in the browser, from one template.
The usual cost of server-side rendering is keeping two implementations in step — one that builds the first page and another that redraws it afterwards. Here both are the same string compiled by the same library.
<style>
body { font: 14px/1.5 system-ui, sans-serif; margin: 0; color: #24292f; }
table { border-collapse: collapse; width: 100%; }
th, td { padding: 6px 10px; border-bottom: 1px solid #eaeef2; text-align: left; }
th { font-weight: 600; background: #f6f8fa; font-size: 12px;
text-transform: uppercase; letter-spacing: .03em; color: #57606a; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
.up { color: #1a7f37; } .down { color: #cf222e; }
button { font: inherit; padding: 6px 14px; margin-top: 14px; cursor: pointer;
border: 1px solid #d8dee4; border-radius: 6px; background: #f6f8fa; }
.stamp { font-size: 12px; color: #8c959f; margin-top: 8px; }
</style>
<div id="app"></div>
<button onclick="tick()">New prices from the server</button>
<div class="stamp" id="stamp"></div>
<script src="https://cdn.jsdelivr.net/gh/richdafunk/hashJS@v1.3.6/hashJS.js"></script>
<script>
// One template. In a real project this is a file that the server reads
// from disk and the browser fetches over HTTP — the same bytes both times.
const TEMPLATE = [
'<table>',
' <thead><tr><th>Ticker</th><th>Name</th><th class="num">Price</th><th class="num">Change</th></tr></thead>',
' <tbody>',
' #for(let r of rows) {#',
' <tr>',
' <td><a href="/quote/#r.ticker#">#r.ticker#</a></td>',
' <td>#htmlEncode(r.name)#</td>',
' <td class="num">#r.price.toFixed(2)#</td>',
' <td class="num #r.change >= 0 ? \'up\' : \'down\'#">#r.change > 0 ? "+" : ""##r.change.toFixed(2)#%</td>',
' </tr>',
' #}#',
' </tbody>',
'</table>'
].join('\n');
const rows = [
{ ticker: 'EQNR', name: 'Equinor ASA', price: 271.40, change: 1.24 },
{ ticker: 'DNB', name: 'DNB Bank ASA', price: 224.90, change: -0.38 },
{ ticker: 'TEL', name: 'Telenor ASA', price: 138.15, change: 0.62 },
{ ticker: 'MOWI', name: 'Mowi ASA', price: 196.05, change: -1.10 }
];
// Compile once. The server does this at startup; the browser does it on
// load. Either way it is the same template producing the same function.
const render = hashJS.compile(TEMPLATE);
const app = document.getElementById('app');
// 1. First paint. On a server this string is already in the response body
// before the browser has run a line of JavaScript.
app.innerHTML = render({ rows: rows });
stampIt('first paint — the string a server would have sent');
// 2. Every update afterwards. Same compiled template, new data, and no
// second implementation to keep in step with the first.
function tick() {
for (const r of rows) {
const move = (Math.random() - 0.48) * 4;
r.price = Math.max(1, r.price + move);
r.change = move;
}
app.innerHTML = render({ rows: rows });
stampIt('re-rendered in the browser from that same template');
}
function stampIt(text) {
document.getElementById('stamp').textContent = text;
}
</script> hashJS.compile returns a function. The server calls it to fill the response body; the browser calls it again whenever the data changes. Neither side knows or cares which one ran first.
That is the practical argument for one language across both tiers. A template written for the browser already runs on a server, and a template written for a server already runs in the browser — there is no port, no second dialect and no build step between them.
The change column shows an expression inside an attribute and two expressions side by side: #r.change > 0 ? "+" : ""##r.change.toFixed(2)#. Substitution happens on the template text before any of it is markup, so position places no restrictions on where an expression may go.