Measure UTF-8 bytes instead of JavaScript string length

A teaching fixture that separates JavaScript string length from UTF-8 byteLength before any packaging limit is applied.
A source-size check can undercount non-ASCII text when it treats JavaScript string length as transmitted bytes. Playable engineers and QA owners who gate builds on that unit risk approving text that later fails a network package check, or rejecting ASCII that was never the real problem. This article is a teaching laboratory: it makes the unit conversion explicit before any packaging limit is applied.
What the documentation establishes
MDN’s TextEncoder.encode() reference states that the method takes a string and returns a Uint8Array containing that string encoded as UTF-8. The return value’s .byteLength is therefore a byte count of UTF-8 text, which is a different measure from JavaScript’s .length on the same string. MDN documents the primitive; the fixture policy around it is ours. Body read on 19 September 2026; the page records a last modification of 28 June 2025.
The failure the fixture isolates
The fixed branch encodes the supplied string and counts bytes: (value) => new TextEncoder().encode(value).byteLength. The broken branch returns value.length. Three synthetic inputs expose the difference without a large bundle: plain ASCII, one accented character, and one emoji. The fixture measures uncompressed UTF-8 text only. It does not measure an archive, a transfer size, a decoded runtime payload or any ad-network package limit.
| Case | Synthetic input | Expected UTF-8 bytes (fixed) | Broken output (`value.length`) |
|---|---|---|---|
| ASCII | `"abc"` | 3 | 3 |
| Accented character | `"é"` | 2 | 1 |
| Emoji | `"🎮"` | 4 | 2 |
On 19 September 2026 the fixed branch matched all three expected values when executed in Node against these authored cases. ASCII happens to agree on both measures; the accented and emoji cases diverge. That pattern is the teaching point: a check that only samples ASCII will not catch the unit error.
Runtime matrix and untested cells
| Cell | Status in this article |
|---|---|
| Node fixture execution (synthetic cases) | Executed 19 September 2026; fixed branch matched expectations |
| Browser device panel | Not executed |
| Host SDK / WebView packaging | Not executed |
| Ad-network package, compressed or decoded size gates | Not claimed |
| Store-opening callbacks | Not tested |
| Campaign performance or analytics delivery | Not claimed |
A browser or Node pass is not a device panel. Untested cells stay empty on purpose.
What a pass does not prove
A pass proves the unit conversion for the supplied ASCII, accented and emoji examples. It does not establish that a playable meets any ad-network size requirement. A platform may specify a package, compressed file, decoded payload or another unit; that exact current requirement must be checked before a submission claim is made. No network reviewed this build, and no campaign result is inferred from the fixture.
Next check on a real build
Label every build-size metric with its unit and stage: source text, encoded bytes, bundled output and final package. Measure the actual submission artifact, not a convenient precursor. Keep source-map and embedded-media decisions visible so a small code change cannot conceal a much larger asset contribution.
Download the case record. It keeps synthetic inputs, fixed and broken outputs, the runtime matrix and the explicit limits with a QA ticket.
Background in the archive: Playable ad device testing matrix and Playable spec sheet before you build. Those guides describe practice; they are not a substitute for this fixture, and this check does not recertify their every claim.


