Skip to content
Chrome 154 → 155

Chrome 155: what changed for fingerprinting and automation

Chrome 155 changes what a page can observe in 29 places, and for the first time in this series a server can tell it from the version before without reading a version string: JPEG XL is on by default, so navigation and image requests advertise image/jxl. Web Crypto gains post-quantum algorithms. The TLS handshake is unchanged in a fresh profile.

Provisional — not yet re-measured on the stable build

Chrome 155 reaches the stable channel on 6 October 2026, and has been rolling out to a small fraction of Windows and macOS users since 30 September as 155.0.8059.26. The values on this page were measured on the 155 beta (155.0.8059.12) against stable 154.0.8037.57, because Google's Linux package pool did not yet carry a stable 155 build when this was published. Everything here was measured three times on both builds, and between a beta and its stable release a value can still move: on the previous release six did. The four most likely to are JPEG XL, Intl locale variants, Intl week year and the WebGPU snorm10 formats, plus the field-trial note under TLS. We re-measure on the stable build and correct this page; the “Run the checks in this browser” button above always reports what your own Chrome actually does.

Pim
Pim · Clearcote Research
Stable release
October 6, 2026
Measured on (Linux x64)
154.0.8037.57 → 155.0.8059.12
Source read at
154.0.8037.58 → 155.0.8059.12
V8
15.4.80.11 → 15.5.35.7

Key takeaways

  • JPEG XL decoding is on by default: ImageDecoder.isTypeSupported('image/jxl') is true, and image/jxl joins the Accept header of every navigation and image request, which a server sees on the first request.
  • Web Crypto gains the post-quantum ML-KEM and ML-DSA, ChaCha20-Poly1305 and X-Wing, five new SubtleCrypto methods for key encapsulation, and a static SubtleCrypto.supports().
  • New CSS a stylesheet test sees at once: margin-trim and text-decoration-skip-spaces (each adds a computed-style property), symbols(), and alpha() now requires an explicit alpha.
  • navigator.getGamepads() has 8 slots instead of 4, with or without a gamepad, and the Soft Navigations interfaces move within window's property order.
  • A server sees the same TLS and HTTP/2 fingerprints as on 152 to 154 in a fresh profile. The new GREASE brand is Not(A:Brand version 24, with Google Chrome listed first.

Every check, both versions, and your browser

36 one-line checks across 29 changes. The strip shows where they are; the table shows what each returns on Chrome 154 and Chrome 155. Run them in this browser to see which version its engine matches.

Chrome 154 → Chrome 155 · 36 checks

Run the checks to see which Chrome version this browser's engine matches.

Where the changes are. Select a surface to filter the rows.

CheckChrome 154Chrome 155This browser
Header order
JPEG XL decoded by default, and image/jxl added to the Accept headerImageDecoder.isTypeSupported('image/jxl')falsetrue–
Client Hints
New GREASE brand and brand order in User-Agent Client Hintsnavigator.userAgentData.brands.map((b) => `${b.brand};v=${b.version}`).join(', ')"Chromium;v=154, Google Chrome;v=154, Not A(Brand;v=99""Google Chrome;v=155, Chromium;v=155, Not(A:Brand;v=24"–
JavaScript and DOM
WebCrypto: ML-KEM, ML-DSA, ChaCha20-Poly1305 and X-Wing, with SubtleCrypto.supports()typeof SubtleCrypto.supports"undefined""function"–
WebCrypto: ML-KEM, ML-DSA, ChaCha20-Poly1305 and X-Wing, with SubtleCrypto.supports()crypto.subtle.generateKey('ChaCha20-Poly1305', false, ['encrypt']).then((k) => k.algorithm.name, (e) => e.name)"NotSupportedError""ChaCha20-Poly1305"–
WebCrypto: ML-KEM, ML-DSA, ChaCha20-Poly1305 and X-Wing, with SubtleCrypto.supports()Object.getOwnPropertyNames(SubtleCrypto.prototype).length1318–
Three more permissions-policy features, and Digital Credentials issuance['digital-credentials-create', 'media-playback-while-not-visible', 'speaker-selection'].map((f) => document.featurePolicy.features().includes(f)).join()"false,false,false""true,true,true"–
Three more permissions-policy features, and Digital Credentials issuance(() => { let read = false; navigator.credentials.create({ get digital() { read = true; } }).catch(() => {}); return read; })()falsetrue–
IntersectionObserver margins accept absolute length units(() => { try { return new IntersectionObserver(() => {}, { rootMargin: '1in' }).rootMargin; } catch (e) { return e.name; } })()"SyntaxError""96px 96px 96px 96px"–
fetch() of a blob: URL rejects an invalid Range headerfetch(URL.createObjectURL(new Blob(['abc'])), { headers: { Range: 'bytes=x' } }).then((r) => r.status, (e) => e.name)200"TypeError"n/a
SVGAElement.referrerPolicy only reflects known values(() => { const a = document.createElementNS('http://www.w3.org/2000/svg', 'a'); a.setAttribute('referrerpolicy', 'bogus'); return a.referrerPolicy; })()"bogus"""–
RTCDTMFToneChangeEvent no longer needs an init dictionaryRTCDTMFToneChangeEvent.length21–
document.<name> finds object and embed elements that are not 'exposed'(() => { const o = document.body.appendChild(document.createElement('object')); o.name = 'cc155probe'; o.appendChild(document.createElement('embed')); const r = document.cc155probe === o; o.remove(); return r; })()falsetrue–
Empty SVG number, integer, angle and length attributes fall back to the initial value(() => { const t = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence'); t.setAttribute('numOctaves', ''); return t.numOctaves.baseVal; })()01–
fetchLater() accepts an empty body(() => { const c = new AbortController(); try { fetchLater(location.href, { method: 'POST', body: '', signal: c.signal }); return 'ok'; } catch (e) { return e.name; } finally { c.abort(); } })()"TypeError""ok"–
<body> margin attributes parse as whole pixelsnew Promise((r) => { const f = document.createElement('iframe'); f.srcdoc = '<body marginwidth="10.5">'; f.onload = () => { r(getComputedStyle(f.contentDocument.body).marginLeft); f.remove(); }; document.body.appendChild(f); })"10.5px""10px"–
Up to 8 gamepads instead of 4navigator.getGamepads().length48–
Soft Navigations interfaces move to their regular place in windowObject.getOwnPropertyNames(window)[Object.getOwnPropertyNames(window).indexOf('PerformanceTiming') + 1]"PerformanceServerTiming""PerformanceSoftNavigation"–
Soft Navigations interfaces move to their regular place in windowObject.getOwnPropertyNames(window)[Object.getOwnPropertyNames(window).indexOf('InterestEvent') + 1]"InputEvent""InteractionContentfulPaint"–
Soft Navigations interfaces move to their regular place in windowObject.getOwnPropertyNames(PerformanceEntry.prototype).indexOf('navigationId') < Object.getOwnPropertyNames(PerformanceEntry.prototype).indexOf('toJSON')falsetrue–
Parsing and rendering
HarfBuzz 14.4: U+180F is treated as an invisible character(() => { const x = document.createElement('canvas').getContext('2d'); x.font = '20px sans-serif'; return x.measureText('a' + String.fromCharCode(6159) + 'b').width === x.measureText('ab').width; })()falsetruen/a
CSS
Two new CSS properties: margin-trim and text-decoration-skip-spacesCSS.supports('margin-trim', 'block')falsetrue–
Two new CSS properties: margin-trim and text-decoration-skip-spacesCSS.supports('text-decoration-skip-spaces', 'all')falsetrue–
@media (dynamic-range) with no value now matchesmatchMedia('(dynamic-range)').matchesfalsetrue–
CSS symbols() for list-style-typeCSS.supports('list-style-type', 'symbols(cyclic "*")')falsetrue–
CSS alpha() now requires an explicit alphaCSS.supports('color', 'alpha(from red)')truefalse–
color-mix() and palette-mix() accept percentages that add up to zeroCSS.supports('color', 'color-mix(in srgb, red 0%, blue 0%)')falsetrue–
CSSPerspective.toMatrix() clamps depths below 1pxnew CSSPerspective(CSS.px(0.5)).toMatrix().m34-2-1–
::first-line styles decide the first line's height on their own(() => { const h = document.body.appendChild(document.createElement('div')); const r = h.attachShadow({ mode: 'open' }); const s = new CSSStyleSheet(); s.replaceSync('p { font: 16px serif; margin: 0 } p::first-line { font-size: 40px; line-height: 10px }'); r.adoptedStyleSheets = [s]; const p = r.appendChild(document.createElement('p')); p.textContent = 'x'; const v = p.getBoundingClientRect().height === 10; h.remove(); return v; })()falsetrue–
text-decoration-thickness joins transition: all(() => { const d = document.body.appendChild(document.createElement('div')); d.style.textDecorationThickness = '0px'; getComputedStyle(d).textDecorationThickness; d.style.transition = 'all 10s'; d.style.textDecorationThickness = '10px'; const n = d.getAnimations().length; d.remove(); return n; })()01–
stroke-dasharray animations no longer add to the underlying value(() => { const s = document.body.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg')); s.style.strokeDasharray = '1'; const a = s.animate({ strokeDasharray: ['2', '2'] }, { duration: 1e3, composite: 'add' }); a.pause(); const v = getComputedStyle(s).strokeDasharray; a.cancel(); s.remove(); return v; })()"3px""2px"–
Media and GPU
Web Audio loops short buffers without repeating a sample(async () => { const c = new OfflineAudioContext(1, 16, 48e3); const b = c.createBuffer(1, 7, 48e3); b.getChannelData(0).set([1, 2, 3, 4, 5, 6, 7]); const s = c.createBufferSource(); s.buffer = b; s.loop = true; s.connect(c.destination); s.start(); const r = await c.startRendering(); return Array.from(r.getChannelData(0)).join(); })()"1,2,3,4,5,6,7,7,2,3,4,5,6,7,8,2""1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2"–
WebGPU vertex format snorm10-10-10-2 works without a flagnavigator.gpu.requestAdapter().then((a) => a.requestDevice()).then(async (d) => { d.pushErrorScope('validation'); const m = d.createShaderModule({ code: '@vertex fn v(@location(0) p: vec4f) -> @builtin(position) vec4f { return p; } @fragment fn f() -> @location(0) vec4f { return vec4f(1); }' }); d.createRenderPipeline({ layout: 'auto', vertex: { module: m, buffers: [{ arrayStride: 4, attributes: [{ format: 'snorm10-10-10-2', offset: 0, shaderLocation: 0 }] }] }, fragment: { module: m, targets: [{ format: 'rgba8unorm' }] } }); const e = await d.popErrorScope(); d.destroy(); return e ? e.constructor.name : 'ok'; })"GPUValidationError""ok"n/a
Intl and time zones
Temporal dates keep the locale's own pattern in Japanese and ChineseTemporal.PlainDate.from('2026-08-27').toLocaleString('ja-JP', { dateStyle: 'long' })"2026/8/27""2026年8月27日"–
Temporal dates keep the locale's own pattern in Japanese and Chinese((f) => f.format(Temporal.PlainDate.from('2026-08-27')) === f.format(new Date(Date.UTC(2026, 7, 27, 12))))(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeZone: 'UTC' }))falsetrue–
Intl.Locale rejects an underscore in the variants option((f) => { try { return f(); } catch (e) { return e.name; } })(() => new Intl.Locale('en', { variants: 'fonipa_scouse' }).toString())"en-fonipa-scouse""RangeError"–
Intl.DateTimeFormat reports the year for week-year patterns'year' in new Intl.DateTimeFormat('te-u-ca-buddhist', { year: 'numeric', month: 'long' }).resolvedOptions()falsetrue–

A tinted cell differs from the version before it. Values are as the DevTools console prints them. Checks that need a debugger attached are not run live.

TLS, HTTP/2 and request headers in Chrome 155

TLS ClientHello
Identical to Chrome 154 in a fresh profile: JA4 t13d1517h2_8daaf6152771_cb7bf5808d99, the same 19 extensions, key-share groups (X25519MLKEM768 first) and signature algorithms, measured three times against two independent TLS echo services. With Google's beta field-trial configuration applied, 7 of 11 Chrome 155 beta profiles sent one extra extension (code point 4832), which changes JA4 to t13d1518h2_8daaf6152771_4980c97edce0. The configuration Google serves to stable 155 has not been measured yet.
HTTP/2
Identical: Akamai fingerprint 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p, priority u=0, i, pseudo-header order m,a,s,p.
Request headers
The same names in the same order on every request type but one, with three value changes. image/jxl joins the Accept header of navigations (before image/avif) and image requests (first). Audio and video range requests send Accept-Encoding: identity instead of identity;q=1, *;q=0, now just before Accept-Language instead of fourth. And User-Agent, Sec-CH-UA and the full-version hints carry the new version. So a server can tell 155 from 154 without reading a version string.

Compare your own browser's handshake with these in the JA4 fingerprint checker.

Chrome 155 User-Agent strings and Client Hints

PlatformUser-Agent
WindowsMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36
macOSMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36
LinuxMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36
AndroidMozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Mobile Safari/537.36
Headless (Linux)Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/155.0.0.0 Safari/537.36

The Linux and headless strings were measured. The others follow Chromium's reduced User-Agent format (user_agent_utils.cc): only the major version is real, the rest is frozen, and the platform part is fixed per operating system.

Sec-CH-UA (the low-entropy brand list sent with every request):

"Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24"

The Chrome user agent page has these for the current version and every recent one; what is my user agent shows your own.

User-Agent and Client Hints

high impactMeasured on both builds

New GREASE brand and brand order in User-Agent Client Hints

The fake GREASE brand and the order of the brand list are computed from the major version. On 155 the brand is Not(A:Brand version 24, and Google Chrome is listed first. Sent on every request in Sec-CH-UA, and returned by navigator.userAgentData.brands in windows and workers.

navigator.userAgentData.brands.map((b) => `${b.brand};v=${b.version}`).join(', ')
Chrome 154
"Chromium;v=154, Google Chrome;v=154, Not A(Brand;v=99"
Chrome 155
"Google Chrome;v=155, Chromium;v=155, Not(A:Brand;v=24"

Why it matters: A version number changed by hand, with the previous brand list left in place, produces a combination no real Chrome sends. The algorithm is unchanged; only its input is.

JavaScript and Web APIs

high impactMeasured on both builds

WebCrypto: ML-KEM, ML-DSA, ChaCha20-Poly1305 and X-Wing, with SubtleCrypto.supports()

The algorithms that were an origin trial in 154 are on for every page. SubtleCrypto gains the static supports() and the encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits and getPublicKey methods, in windows and in all worker scopes.

typeof SubtleCrypto.supports
Chrome 154
"undefined"
Chrome 155
"function"
  • crypto.subtle.generateKey('ChaCha20-Poly1305', false, ['encrypt']).then((k) => k.algorithm.name, (e) => e.name)"NotSupportedError"→ "ChaCha20-Poly1305"
  • Object.getOwnPropertyNames(SubtleCrypto.prototype).length13→ 18

Why it matters: Five new prototype members and a static method change the interface's property list in every realm, and the generateKey answer needs no permission and no network.

high impactMeasured on both builds

Three more permissions-policy features, and Digital Credentials issuance

document.featurePolicy.features() grows from 75 to 78 entries: digital-credentials-create (the Digital Credentials API for issuance ships, so navigator.credentials.create() reads a digital member), media-playback-while-not-visible (pauses media in invisible iframes) and speaker-selection (no longer tied to an experimental flag).

['digital-credentials-create', 'media-playback-while-not-visible', 'speaker-selection'].map((f) => document.featurePolicy.features().includes(f)).join()
Chrome 154
"false,false,false"
Chrome 155
"true,true,true"
  • (() => { let read = false; navigator.credentials.create({ get digital() { read = true; } }).catch(() => {}); return read; })()false→ true

Why it matters: The policy-feature list and its length are cheap, synchronous and permission-free, and they move with every release.

medium impactMeasured on both builds

IntersectionObserver margins accept absolute length units

rootMargin and scrollMargin accept cm, mm, Q, in, pt and pc, converted to CSS pixels. In 154 anything other than px and % threw a SyntaxError.

(() => { try { return new IntersectionObserver(() => {}, { rootMargin: '1in' }).rootMargin; } catch (e) { return e.name; } })()
Chrome 154
"SyntaxError"
Chrome 155
"96px 96px 96px 96px"

Why it matters: Synchronous, permission-free and a single constructor call.

medium impactMeasured on both builds

SVGAElement.referrerPolicy only reflects known values

An unknown referrerpolicy attribute on an SVG <a> now reads back as an empty string, as it already did on HTML links.

(() => { const a = document.createElementNS('http://www.w3.org/2000/svg', 'a'); a.setAttribute('referrerpolicy', 'bogus'); return a.referrerPolicy; })()
Chrome 154
"bogus"
Chrome 155
""

Why it matters: A one-line, deterministic reflection difference.

medium impactMeasured on both builds

Soft Navigations interfaces move to their regular place in window

PerformanceSoftNavigation, InteractionContentfulPaint and PerformanceEntry.prototype.navigationId were already exposed on 154, behind runtime flags that were on. 155 drops the flags, so Blink installs them with the unconditional members: the two interfaces move from the end of window's property list to their alphabetical slots, and navigationId moves ahead of toJSON. The number of globals does not change.

Object.getOwnPropertyNames(window)[Object.getOwnPropertyNames(window).indexOf('PerformanceTiming') + 1]
Chrome 154
"PerformanceServerTiming"
Chrome 155
"PerformanceSoftNavigation"
  • Object.getOwnPropertyNames(window)[Object.getOwnPropertyNames(window).indexOf('InterestEvent') + 1]"InputEvent"→ "InteractionContentfulPaint"
  • Object.getOwnPropertyNames(PerformanceEntry.prototype).indexOf('navigationId') < Object.getOwnPropertyNames(PerformanceEntry.prototype).indexOf('toJSON')false→ true

Why it matters: Any hash of window's property order moves, as it did in 154. The relative checks here do not depend on the platform; absolute indexes do.

low impactMeasured on both builds

fetch() of a blob: URL rejects an invalid Range header

A Range header that is not a single valid byte range used to be ignored and the whole blob returned with status 200. 155 fails the request with a network error, as the Fetch standard requires.

fetch(URL.createObjectURL(new Blob(['abc'])), { headers: { Range: 'bytes=x' } }).then((r) => r.status, (e) => e.name)
Chrome 154
200
Chrome 155
"TypeError"

Why it matters: Pure JavaScript with no network access and no permission, and deterministic on a fresh profile. But the behaviour sits behind a feature switch that Google's field-trial configuration can turn on before 155, and an installed Chrome 154 on Windows that we checked already returned "TypeError". On real installs it does not separate the two versions, so the live column skips it.

low impactMeasured on both builds

fetchLater() accepts an empty body

A zero-length body was mistaken for a stream of unknown length and rejected with a TypeError. The check aborts the deferred request, so nothing is sent.

(() => { const c = new AbortController(); try { fetchLater(location.href, { method: 'POST', body: '', signal: c.signal }); return 'ok'; } catch (e) { return e.name; } finally { c.abort(); } })()
Chrome 154
"TypeError"
Chrome 155
"ok"

Why it matters: Synchronous and permission-free on an https page.

HTML

low impactMeasured on both builds

document.<name> finds object and embed elements that are not 'exposed'

Document named properties used to skip <object> and <embed> elements that HTML calls not exposed (for example an object with an embed inside). The HTML standard dropped that filter and 155 follows it.

(() => { const o = document.body.appendChild(document.createElement('object')); o.name = 'cc155probe'; o.appendChild(document.createElement('embed')); const r = document.cc155probe === o; o.remove(); return r; })()
Chrome 154
false
Chrome 155
true

Why it matters: Deterministic, but it needs a crafted element tree.

low impactMeasured on both builds

Empty SVG number, integer, angle and length attributes fall back to the initial value

An empty string used to parse as 0 for these SVG attribute types. It is now a parse error, so the attribute takes its initial value: numOctaves="" on feTurbulence reads 1, not 0.

(() => { const t = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence'); t.setAttribute('numOctaves', ''); return t.numOctaves.baseVal; })()
Chrome 154
0
Chrome 155
1

Why it matters: Deterministic SVG DOM behaviour with no rendering involved.

low impactMeasured on both builds

<body> margin attributes parse as whole pixels

marginwidth, marginheight, leftmargin and topmargin now follow the HTML rendering rules for pixel lengths: fractions are dropped and percentages are no longer accepted.

new Promise((r) => { const f = document.createElement('iframe'); f.srcdoc = '<body marginwidth="10.5">'; f.onload = () => { r(getComputedStyle(f.contentDocument.body).marginLeft); f.remove(); }; document.body.appendChild(f); })
Chrome 154
"10.5px"
Chrome 155
"10px"

Why it matters: Deterministic, but it needs a document with a legacy attribute.

Parsing and rendering behaviour

medium impactMeasured on both builds

HarfBuzz 14.4: U+180F is treated as an invisible character

Text shaping moves from HarfBuzz 14.3.1 to 14.4.0. U+180F MONGOLIAN FREE VARIATION SELECTOR FOUR is now default-ignorable, so it takes no space; in 154 it drew a missing-glyph box (20 px wide at 20 px on Linux).

(() => { const x = document.createElement('canvas').getContext('2d'); x.font = '20px sans-serif'; return x.measureText('a' + String.fromCharCode(6159) + 'b').width === x.measureText('ab').width; })()
Chrome 154
false
Chrome 155
true

Why it matters: Text-measurement fingerprints probe unusual code points, and this one now measures zero on every platform. Live check off: on a system with a font that draws U+180F with zero width, 154 also reads true.

CSS

high impactMeasured on both builds

Two new CSS properties: margin-trim and text-decoration-skip-spaces

Both leave the experimental set, so the computed style of every element has two more properties (478 to 480 on a page that defines no custom properties). text-decoration-skip-spaces also starts at start end, so an underline no longer runs under leading and trailing spaces.

CSS.supports('margin-trim', 'block')
Chrome 154
false
Chrome 155
true
  • CSS.supports('text-decoration-skip-spaces', 'all')false→ true

Why it matters: Scripts that count or hash computed-style properties see two more, and the new initial value changes how underlined text with spaces is painted.

medium impactMeasured on both builds

@media (dynamic-range) with no value now matches

The boolean form of the dynamic-range media feature always evaluated to false. 155 treats it as true, as Media Queries requires; (dynamic-range: standard) and (dynamic-range: high) are unchanged.

matchMedia('(dynamic-range)').matches
Chrome 154
false
Chrome 155
true

Why it matters: Media-query sweeps are a common probe, and this answer now depends on the version rather than the display.

medium impactMeasured on both builds

CSS alpha() now requires an explicit alpha

alpha(from red) was valid in 154 and is rejected in 155, after a CSS Working Group resolution. alpha(from red / 0.5) is valid in both.

CSS.supports('color', 'alpha(from red)')
Chrome 154
true
Chrome 155
false

Why it matters: One of the few values that becomes invalid, so an older engine claiming 155 answers the wrong way.

medium impactMeasured on both builds

color-mix() and palette-mix() accept percentages that add up to zero

color-mix(in srgb, red 0%, blue 0%) was rejected at parse time. 155 accepts it and resolves it to a transparent colour, as CSS Color 5 specifies.

CSS.supports('color', 'color-mix(in srgb, red 0%, blue 0%)')
Chrome 154
false
Chrome 155
true

Why it matters: A deterministic parser difference in a function that colour probes already use.

low impactMeasured on both builds

::first-line styles decide the first line's height on their own

The FirstLineTextMetrics behaviour, on since 2025, is switched off because the CSS specification was changed to match WebKit and older Blink. A ::first-line line-height now sets the first line's height; in 154 the block's own line height still acted as a minimum.

(() => { const h = document.body.appendChild(document.createElement('div')); const r = h.attachShadow({ mode: 'open' }); const s = new CSSStyleSheet(); s.replaceSync('p { font: 16px serif; margin: 0 } p::first-line { font-size: 40px; line-height: 10px }'); r.adoptedStyleSheets = [s]; const p = r.appendChild(document.createElement('p')); p.textContent = 'x'; const v = p.getBoundingClientRect().height === 10; h.remove(); return v; })()
Chrome 154
false
Chrome 155
true

Why it matters: A layout difference any page with a ::first-line rule can measure.

low impactMeasured on both builds

text-decoration-thickness joins transition: all

The property is now marked interpolable, so transition: all animates a thickness change instead of jumping.

(() => { const d = document.body.appendChild(document.createElement('div')); d.style.textDecorationThickness = '0px'; getComputedStyle(d).textDecorationThickness; d.style.transition = 'all 10s'; d.style.textDecorationThickness = '10px'; const n = d.getAnimations().length; d.remove(); return n; })()
Chrome 154
0
Chrome 155
1

Why it matters: Changes the set of properties transition: all covers.

low impactMeasured on both builds

stroke-dasharray animations no longer add to the underlying value

SVG 2 defines stroke-dasharray as non-additive, so composite: 'add' and 'accumulate' now behave like 'replace'.

(() => { const s = document.body.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg')); s.style.strokeDasharray = '1'; const a = s.animate({ strokeDasharray: ['2', '2'] }, { duration: 1e3, composite: 'add' }); a.pause(); const v = getComputedStyle(s).strokeDasharray; a.cancel(); s.remove(); return v; })()
Chrome 154
"3px"
Chrome 155
"2px"

Why it matters: Deterministic Web Animations behaviour.

Media and codecs

medium impactMeasured on both builds

Web Audio loops short buffers without repeating a sample

AudioBufferSourceNode now keeps loop boundaries in sample frames. In 154 a looped 7-frame buffer at 48 kHz repeated its last sample and skipped the first on each pass, because the boundary was converted to seconds and back.

(async () => { const c = new OfflineAudioContext(1, 16, 48e3); const b = c.createBuffer(1, 7, 48e3); b.getChannelData(0).set([1, 2, 3, 4, 5, 6, 7]); const s = c.createBufferSource(); s.buffer = b; s.loop = true; s.connect(c.destination); s.start(); const r = await c.startRendering(); return Array.from(r.getChannelData(0)).join(); })()
Chrome 154
"1,2,3,4,5,6,7,7,2,3,4,5,6,7,8,2"
Chrome 155
"1,2,3,4,5,6,7,1,2,3,4,5,6,7,1,2"

Why it matters: Rendered offline, so it needs no audio device and gives the same numbers on every machine; audio fingerprints that loop short buffers change.

WebGL and WebGPU

low impactRead in the source at both tags

WebGPU vertex format snorm10-10-10-2 works without a flag

Dawn drops the experimental gate on the snorm10-10-10-2 vertex format. In 154 the IDL accepted the value but pipeline creation raised a validation error unless an unsafe toggle was set.

navigator.gpu.requestAdapter().then((a) => a.requestDevice()).then(async (d) => { d.pushErrorScope('validation'); const m = d.createShaderModule({ code: '@vertex fn v(@location(0) p: vec4f) -> @builtin(position) vec4f { return p; } @fragment fn f() -> @location(0) vec4f { return vec4f(1); }' }); d.createRenderPipeline({ layout: 'auto', vertex: { module: m, buffers: [{ arrayStride: 4, attributes: [{ format: 'snorm10-10-10-2', offset: 0, shaderLocation: 0 }] }] }, fragment: { module: m, targets: [{ format: 'rgba8unorm' }] } }); const e = await d.popErrorScope(); d.destroy(); return e ? e.constructor.name : 'ok'; })
Chrome 154
"GPUValidationError"
Chrome 155
"ok"

Why it matters: Needs a WebGPU adapter, so it only separates versions on machines where WebGPU works.

Network: TLS, HTTP/2 and headers

high impactMeasured on both builds

JPEG XL decoded by default, and image/jxl added to the Accept header

The JXLImageFormat feature flips from disabled to enabled, so Chrome decodes JPEG XL images and says so. Navigations now send Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7, and image requests send image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8.

ImageDecoder.isTypeSupported('image/jxl')
Chrome 154
false
Chrome 155
true

Why it matters: The Accept header goes out with every navigation and every image request, so a server sees the change without running a script. A 155 claim without image/jxl, or a 154 claim with it, contradicts itself.

Intl, time zones and locale

high impactMeasured on both builds

Temporal dates keep the locale's own pattern in Japanese and Chinese

V8 15.5 formats Temporal values with the same ICU pattern as Date. Chrome 154 rebuilt the pattern from a skeleton and lost the 年, 月 and 日 literals, so a Japanese long date read 2026/8/27.

Temporal.PlainDate.from('2026-08-27').toLocaleString('ja-JP', { dateStyle: 'long' })
Chrome 154
"2026/8/27"
Chrome 155
"2026年8月27日"
  • ((f) => f.format(Temporal.PlainDate.from('2026-08-27')) === f.format(new Date(Date.UTC(2026, 7, 27, 12))))(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeZone: 'UTC' }))false→ true

Why it matters: Deterministic in every realm and independent of the host's locale settings, because the engine carries its own ICU data.

medium impactMeasured on both builds

Intl.Locale rejects an underscore in the variants option

ICU turns _ into - before validating variants, so 154 accepted fonipa_scouse. V8 15.5 validates each subtag first and throws a RangeError.

((f) => { try { return f(); } catch (e) { return e.name; } })(() => new Intl.Locale('en', { variants: 'fonipa_scouse' }).toString())
Chrome 154
"en-fonipa-scouse"
Chrome 155
"RangeError"

Why it matters: A silent validation difference in a constructor that locale probes already call.

medium impactMeasured on both builds

Intl.DateTimeFormat reports the year for week-year patterns

Some locale and calendar pairs, such as Telugu with the Buddhist calendar, produce week-year patterns. 154 left year out of resolvedOptions() (and its formatToParts() could not handle the field); 155 maps it to year.

'year' in new Intl.DateTimeFormat('te-u-ca-buddhist', { year: 'numeric', month: 'long' }).resolvedOptions()
Chrome 154
false
Chrome 155
true

Why it matters: Deterministic and cheap; ICU data is bundled with the engine.

What the docs claim that the source does not show

Release notes and Chrome Platform Status describe intent; the source at the release tag is what shipped. These entries differ, so a version check built on the docs alone would be wrong.

Reproduce these results

  1. 1. Get both versions

    Use Google Chrome's own Linux packages of the two exact versions. Google keeps every version in its package pool:

    bash
    curl -sSO https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_154.0.8037.57-1_amd64.deb
    curl -sSO https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-beta/google-chrome-beta_155.0.8059.12-1_amd64.deb
    for f in google-chrome-*.deb; do dpkg -x "$f" "${f%.deb}"; done
  2. 2. Start each one clean

    A new profile directory per run and no automation flags. A fresh profile has not applied Google's field-trial configuration yet, which is the state these values describe. Headless (--headless=new) gives the same values as headful for everything on this page.

    bash
    ./google-chrome-beta_155.0.8059.12-1_amd64/opt/google/chrome-beta/chrome --user-data-dir="$(mktemp -d)" --no-first-run https://example.com
  3. 3. Run the checks

    Press "Run the checks in this browser" at the top of this page, or paste the script below into the DevTools console on any https:// page. Several checks are asynchronous; the script awaits them.

  4. 4. Check the network side

    Open tls.peet.ws or tls.browserleaks.com in each version and compare the JA4 and HTTP/2 fingerprints and the Accept header with the values above.

  5. 5. Find a change in the source yourself

    Blink's test expectations record the page-visible surface of each release; the runtime feature list records what is on by default:

    bash
    git diff 154.0.8037.58 155.0.8059.12 -- third_party/blink/web_tests/virtual/stable/webexposed/
    git diff 154.0.8037.58 155.0.8059.12 -- third_party/blink/renderer/platform/runtime_enabled_features.json5

The whole check list as one script

Paste it into the DevTools console on Chrome 154 and on Chrome 155: it prints every check on this page with the value your browser returns.

chrome-155-probe.js
// Chrome 154 vs 155: paste into the DevTools console on any https:// page and press Enter.
// Each line prints "<change>  <check> = <value>". Expected values: clearcotelabs.com/chrome-releases/155
(async () => {
  const checks = [
    ["grease-brand", () => navigator.userAgentData.brands.map((b) => `${b.brand};v=${b.version}`).join(', ')],
    ["jpeg-xl", () => ImageDecoder.isTypeSupported('image/jxl')],
    ["webcrypto-modern-algorithms", () => typeof SubtleCrypto.supports],
    ["webcrypto-modern-algorithms", () => crypto.subtle.generateKey('ChaCha20-Poly1305', false, ['encrypt']).then((k) => k.algorithm.name, (e) => e.name)],
    ["webcrypto-modern-algorithms", () => Object.getOwnPropertyNames(SubtleCrypto.prototype).length],
    ["new-css-properties", () => CSS.supports('margin-trim', 'block')],
    ["new-css-properties", () => CSS.supports('text-decoration-skip-spaces', 'all')],
    ["permissions-policy-features", () => ['digital-credentials-create', 'media-playback-while-not-visible', 'speaker-selection'].map((f) => document.featurePolicy.features().includes(f)).join()],
    ["permissions-policy-features", () => (() => { let read = false; navigator.credentials.create({ get digital() { read = true; } }).catch(() => {}); return read; })()],
    ["temporal-cjk-format", () => Temporal.PlainDate.from('2026-08-27').toLocaleString('ja-JP', { dateStyle: 'long' })],
    ["temporal-cjk-format", () => ((f) => f.format(Temporal.PlainDate.from('2026-08-27')) === f.format(new Date(Date.UTC(2026, 7, 27, 12))))(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeZone: 'UTC' }))],
    ["dynamic-range-boolean", () => matchMedia('(dynamic-range)').matches],
    ["css-symbols-function", () => CSS.supports('list-style-type', 'symbols(cyclic "*")')],
    ["css-alpha-requires-alpha", () => CSS.supports('color', 'alpha(from red)')],
    ["color-mix-zero-percent", () => CSS.supports('color', 'color-mix(in srgb, red 0%, blue 0%)')],
    ["intersection-observer-units", () => (() => { try { return new IntersectionObserver(() => {}, { rootMargin: '1in' }).rootMargin; } catch (e) { return e.name; } })()],
    ["blob-url-range", () => fetch(URL.createObjectURL(new Blob(['abc'])), { headers: { Range: 'bytes=x' } }).then((r) => r.status, (e) => e.name)],
    ["audio-buffer-loop", () => (async () => { const c = new OfflineAudioContext(1, 16, 48e3); const b = c.createBuffer(1, 7, 48e3); b.getChannelData(0).set([1, 2, 3, 4, 5, 6, 7]); const s = c.createBufferSource(); s.buffer = b; s.loop = true; s.connect(c.destination); s.start(); const r = await c.startRendering(); return Array.from(r.getChannelData(0)).join(); })()],
    ["svg-a-referrerpolicy", () => (() => { const a = document.createElementNS('http://www.w3.org/2000/svg', 'a'); a.setAttribute('referrerpolicy', 'bogus'); return a.referrerPolicy; })()],
    ["dtmf-event-init", () => RTCDTMFToneChangeEvent.length],
    ["intl-locale-variants", () => ((f) => { try { return f(); } catch (e) { return e.name; } })(() => new Intl.Locale('en', { variants: 'fonipa_scouse' }).toString())],
    ["intl-week-year", () => 'year' in new Intl.DateTimeFormat('te-u-ca-buddhist', { year: 'numeric', month: 'long' }).resolvedOptions()],
    ["harfbuzz-u180f", () => (() => { const x = document.createElement('canvas').getContext('2d'); x.font = '20px sans-serif'; return x.measureText('a' + String.fromCharCode(6159) + 'b').width === x.measureText('ab').width; })()],
    ["document-named-object", () => (() => { const o = document.body.appendChild(document.createElement('object')); o.name = 'cc155probe'; o.appendChild(document.createElement('embed')); const r = document.cc155probe === o; o.remove(); return r; })()],
    ["svg-empty-attribute", () => (() => { const t = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence'); t.setAttribute('numOctaves', ''); return t.numOctaves.baseVal; })()],
    ["css-perspective-clamp", () => new CSSPerspective(CSS.px(0.5)).toMatrix().m34],
    ["fetchlater-empty-body", () => (() => { const c = new AbortController(); try { fetchLater(location.href, { method: 'POST', body: '', signal: c.signal }); return 'ok'; } catch (e) { return e.name; } finally { c.abort(); } })()],
    ["first-line-metrics", () => (() => { const h = document.body.appendChild(document.createElement('div')); const r = h.attachShadow({ mode: 'open' }); const s = new CSSStyleSheet(); s.replaceSync('p { font: 16px serif; margin: 0 } p::first-line { font-size: 40px; line-height: 10px }'); r.adoptedStyleSheets = [s]; const p = r.appendChild(document.createElement('p')); p.textContent = 'x'; const v = p.getBoundingClientRect().height === 10; h.remove(); return v; })()],
    ["text-decoration-thickness-transition", () => (() => { const d = document.body.appendChild(document.createElement('div')); d.style.textDecorationThickness = '0px'; getComputedStyle(d).textDecorationThickness; d.style.transition = 'all 10s'; d.style.textDecorationThickness = '10px'; const n = d.getAnimations().length; d.remove(); return n; })()],
    ["stroke-dasharray-additive", () => (() => { const s = document.body.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg')); s.style.strokeDasharray = '1'; const a = s.animate({ strokeDasharray: ['2', '2'] }, { duration: 1e3, composite: 'add' }); a.pause(); const v = getComputedStyle(s).strokeDasharray; a.cancel(); s.remove(); return v; })()],
    ["body-margin-pixel-length", () => new Promise((r) => { const f = document.createElement('iframe'); f.srcdoc = '<body marginwidth="10.5">'; f.onload = () => { r(getComputedStyle(f.contentDocument.body).marginLeft); f.remove(); }; document.body.appendChild(f); })],
    ["webgpu-snorm10", () => navigator.gpu.requestAdapter().then((a) => a.requestDevice()).then(async (d) => { d.pushErrorScope('validation'); const m = d.createShaderModule({ code: '@vertex fn v(@location(0) p: vec4f) -> @builtin(position) vec4f { return p; } @fragment fn f() -> @location(0) vec4f { return vec4f(1); }' }); d.createRenderPipeline({ layout: 'auto', vertex: { module: m, buffers: [{ arrayStride: 4, attributes: [{ format: 'snorm10-10-10-2', offset: 0, shaderLocation: 0 }] }] }, fragment: { module: m, targets: [{ format: 'rgba8unorm' }] } }); const e = await d.popErrorScope(); d.destroy(); return e ? e.constructor.name : 'ok'; })],
    ["gamepad-slots", () => navigator.getGamepads().length],
    ["soft-navigation-order", () => Object.getOwnPropertyNames(window)[Object.getOwnPropertyNames(window).indexOf('PerformanceTiming') + 1]],
    ["soft-navigation-order", () => Object.getOwnPropertyNames(window)[Object.getOwnPropertyNames(window).indexOf('InterestEvent') + 1]],
    ["soft-navigation-order", () => Object.getOwnPropertyNames(PerformanceEntry.prototype).indexOf('navigationId') < Object.getOwnPropertyNames(PerformanceEntry.prototype).indexOf('toJSON')],
  ];
  const out = [];
  for (const [id, f] of checks) {
    let v;
    try { v = await f(); } catch (e) { v = "throws " + e.name; }
    const expr = f.toString().replace(/^\(\)\s*=>\s*/, "");
    out.push(id.padEnd(28) + expr + " = " + (typeof v === "string" ? JSON.stringify(v) : String(v)));
  }
  console.log(out.join("\n"));
})();

FAQ

When is Chrome 155 released?

Chrome 155's stable date is October 6, 2026, two weeks after Chrome 154. An early-stable rollout to a small share of users started on September 23.

What is the Chrome 155 user agent string?

On Windows: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36. Only the major version changes; the full version travels in Client Hints. The table above lists every platform.

What is the Sec-CH-UA header in Chrome 155?

"Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24". Both the fake GREASE brand and the order change with every major.

Why does Chrome 155 send image/jxl in the Accept header?

JPEG XL decoding is on by default from 155, and Chrome advertises every image format it decodes. A request that claims Chrome 155 without image/jxl in a navigation's Accept header did not come from a default Chrome 155.

Did Chrome 155 change the TLS (JA3/JA4) or HTTP/2 fingerprint?

Not in a fresh profile: JA4 t13d1517h2_8daaf6152771_cb7bf5808d99 and the same HTTP/2 settings, as on 152 to 154. Google's field-trial configuration can add a TLS extension on some installs; see the network section.

How can a website tell Chrome 154 from Chrome 155?

The quickest lines are ImageDecoder.isTypeSupported('image/jxl'), typeof SubtleCrypto.supports and navigator.getGamepads().length, which is 8 from 155.

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

Free for one browser with GitHub. No card.