feat: add performance monitoring for journey transitions and optimize rendering

- Introduced a new markdown file documenting the optimizations made to the journey motion rendering, including changes to CSS and React components to improve performance on low-end devices.
- Implemented a test-only JavaScript file to log layout work during automatic stop changes, tracking metrics such as name resizes, font writes, and heading replacements.
- Enhanced the journey rendering by memoizing components and reducing unnecessary re-renders, aligning with web.dev recommendations for animations and layout thrashing prevention.
This commit is contained in:
2026-10-11 15:13:05 +03:00
parent a1bb3f1c9d
commit 823ab14367
7 changed files with 167 additions and 43 deletions

View File

@@ -116,25 +116,27 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.journey-order { color: var(--accent); font-size: .85em; white-space: nowrap; }
.journey-map-space { flex: 1 1 0; min-height: 0; display: flex; align-items: center; margin: 18px -8px 0; }
.journey-rail-window { width: 100%; height: min(100%, 320px); position: relative; overflow: hidden; }
.journey-rail-window::after { content: ''; position: absolute; inset: 0 0 0 auto; width: 36px; background: linear-gradient(90deg, transparent, var(--background)); pointer-events: none; }
.journey-rail { display: flex; height: 100%; margin: 0; padding: 0 0 0 24px; list-style: none; transform: translateX(calc(-1 * var(--journey-focus) * var(--station-width))); transition: transform 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station { flex: 0 0 var(--station-width); min-width: 0; position: relative; display: grid; grid-template-rows: auto minmax(0, 1fr) 40px; gap: 10px; padding-right: 28px; transition: flex-basis 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station[data-focused=true] { flex-basis: var(--focused-width); }
.journey-station::after { content: ''; position: absolute; bottom: 17.5px; left: 0; width: 100%; height: 5px; background: var(--accent); transition: opacity 700ms ease; }
.journey-station:last-child::after { width: calc(var(--station-width) - 36px); }
.journey-metro[data-more=false] .journey-station:last-child::after { display: none; }
.journey-station[data-before=true]::after { opacity: .25; }
.journey-station-name { min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; padding-bottom: 8px; transition: opacity 500ms ease; }
.journey-station[data-before=true] .journey-station-name, .journey-station[data-before=true] .journey-duration { opacity: 0; }
.journey-rail-window::after { content: ''; position: absolute; inset: 0 0 0 auto; width: 36px; z-index: 3; background: linear-gradient(90deg, transparent, var(--background)); pointer-events: none; }
.journey-rail { position: relative; display: flex; height: 100%; margin: 0; padding: 0 0 0 24px; list-style: none; }
.journey-line { position: absolute; left: 24px; bottom: 17.5px; width: calc(100% - 24px); height: 5px; background: var(--accent); transform-origin: left center; transition: transform 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station, .journey-continuation { --journey-space: 0px; transform: translate3d(calc(-1 * var(--journey-focus) * var(--station-width) + var(--journey-space)), 0, 0); transition: transform 900ms cubic-bezier(.22,.72,.18,1); }
.journey-station { flex: 0 0 var(--station-width); min-width: 0; position: relative; display: grid; grid-template-rows: auto minmax(0, 1fr) 40px; gap: 10px; }
.journey-station[data-after=true], .journey-continuation { --journey-space: calc(var(--focused-width) - var(--station-width)); }
.journey-metro[data-active=true] .journey-station, .journey-metro[data-active=true] .journey-continuation, .journey-metro[data-active=true] .journey-line { will-change: transform; }
.journey-station-name { min-width: 0; min-height: 0; position: relative; }
.journey-name-layer { position: absolute; inset: 0 auto 0 0; display: flex; flex-direction: column; justify-content: flex-end; padding: 0 28px 8px 0; opacity: 0; transition: opacity 300ms ease-out, transform 900ms cubic-bezier(.22,.72,.18,1); }
.journey-name-context { width: var(--station-width); }
.journey-name-focus { width: var(--focused-width); transform: translate3d(0, 0, 0) scale(var(--journey-name-scale, .4)); transform-origin: left bottom; }
.journey-station[data-focused=true] .journey-name-focus { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
.journey-station[data-focused=false][data-before=false] .journey-name-context { opacity: 1; }
.journey-station[data-before=true] .journey-duration { opacity: 0; }
.service-info .journey-station-name .destination-heading { flex-shrink: 0; margin: 0; font-size: calc(clamp(22px, 4cqw, 32px) * var(--reading-scale, 1)); line-height: 1.13; font-weight: 600; letter-spacing: -.035em; overflow-wrap: anywhere; transition: color 600ms ease; }
.service-info .journey-station[data-focused=true] .destination-heading { font-size: calc(clamp(48px, 11cqw, 96px) * var(--reading-scale, 1)); font-weight: 650; letter-spacing: -.055em; color: var(--foreground); }
.journey-station[data-focused=true] .destination-current { transform-origin: left bottom; animation: journey-name-in 900ms cubic-bezier(.22,.72,.18,1) both; }
@keyframes journey-name-in { from { opacity: 0; transform: translateX(20px) scale(.84); } to { opacity: 1; transform: translateX(0) scale(1); } }
.journey-duration { align-self: start; display: flex; align-items: baseline; gap: 8px; color: var(--muted); font-size: clamp(18px, 3.4cqw, 26px); font-weight: 550; line-height: 1; white-space: nowrap; transition: color 600ms ease, opacity 500ms ease; }
.service-info .journey-name-focus .destination-heading { font-size: calc(clamp(48px, 11cqw, 96px) * var(--reading-scale, 1)); font-weight: 650; letter-spacing: -.055em; color: var(--foreground); }
.journey-duration { align-self: start; display: flex; align-items: baseline; gap: 8px; color: var(--muted); font-size: clamp(24px, 4.5cqw, 36px); font-weight: 550; line-height: 1; white-space: nowrap; transition: opacity 300ms ease-out; }
.journey-duration > span { display: inline-flex; align-items: baseline; gap: 5px; }
.journey-duration small { font-size: .72em; font-weight: 500; }
.journey-station[data-focused=true] .journey-duration { color: var(--accent); font-size: clamp(24px, 4.5cqw, 36px); }
.journey-node { align-self: center; display: block; width: 24px; height: 24px; border: 4px solid var(--accent); border-radius: 50%; background: var(--background); margin-left: -12px; z-index: 1; transition: background 600ms ease, box-shadow 600ms ease; }
.journey-station[data-focused=true] .journey-duration { color: var(--accent); }
.journey-node { align-self: center; display: block; width: 24px; height: 24px; border: 4px solid var(--accent); border-radius: 50%; background: var(--background); margin-left: -12px; z-index: 1; }
.journey-station[data-focused=true] .journey-node { background: var(--accent); box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 18%, transparent); }
.journey-station[data-before=true] .journey-node { border-color: var(--outline); }
.journey-continuation { flex: 0 0 var(--station-width); min-width: 0; display: flex; align-items: center; position: relative; padding: 34px 20px 40px 0; }
@@ -143,19 +145,18 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.journey-caption { display: flex; flex-wrap: wrap; gap: 4px 14px; justify-content: space-between; color: var(--muted); font-size: 16px; line-height: 1.3; padding-top: 16px; }
.journey-caption-compact { display: none; }
.journey-page { color: var(--accent); margin-left: auto; }
.journey-metro[data-calm=true] .journey-rail { transition: transform 300ms ease-out; }
.journey-metro[data-calm=true] .journey-station { transition: flex-basis 300ms ease-out; }
.journey-metro[data-calm=true] .destination-heading { transition: color 250ms ease-out; }
.journey-metro[data-calm=true] .destination-current { animation: none; }
.journey-metro[data-loop=true] .journey-rail { transition: none; animation: journey-overview-reset 600ms ease-out both; }
.journey-metro[data-loop=true] .journey-station { transition: none; }
.journey-metro[data-calm=true] .journey-station, .journey-metro[data-calm=true] .journey-continuation, .journey-metro[data-calm=true] .journey-line { transition: transform 300ms ease-out; }
.journey-metro[data-calm=true] .journey-name-layer { transition: opacity 180ms ease-out, transform 300ms ease-out; }
.journey-metro[data-loop=true] .journey-rail-window { animation: journey-overview-reset 600ms ease-out both; }
.journey-metro[data-calm=true][data-loop=true] .journey-rail-window { animation-duration: 180ms; }
.journey-metro[data-loop=true] .journey-station, .journey-metro[data-loop=true] .journey-continuation, .journey-metro[data-loop=true] .journey-line, .journey-metro[data-loop=true] .journey-name-layer { transition: none; }
@keyframes journey-overview-reset { from { opacity: 0; } to { opacity: 1; } }
@container journey-card (max-width: 380px) {
.journey-trip { display: block; }
.journey-trip time { margin-top: 7px; }
.journey-metro { --station-width: 140px; }
.service-info .journey-station[data-focused=true] .destination-heading { font-size: calc(clamp(34px, 11cqw, 46px) * var(--reading-scale, 1)); }
.journey-station { padding-right: 22px; }
.service-info .journey-name-focus .destination-heading { font-size: calc(clamp(34px, 11cqw, 46px) * var(--reading-scale, 1)); }
.journey-name-layer { padding-right: 22px; }
.journey-tour-heading { font-size: 17px; }
.journey-caption { font-size: 14px; gap: 4px 10px; }
}
@@ -167,10 +168,9 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.journey-metro { padding-top: 12px; }
.journey-map-space { margin-top: 10px; }
.journey-station { grid-template-rows: auto minmax(0, 1fr) 32px; gap: 6px; }
.journey-station-name { padding-bottom: 4px; }
.journey-duration { font-size: 22px; }
.journey-station[data-focused=true] .journey-duration { font-size: 26px; }
.journey-station::after { bottom: 14px; height: 4px; }
.journey-name-layer { padding-bottom: 4px; }
.journey-duration { font-size: 26px; }
.journey-line { bottom: 14px; height: 4px; }
.journey-node { width: 22px; height: 22px; margin-left: -11px; border-width: 3px; }
.journey-continuation { padding-top: 26px; padding-bottom: 32px; }
.journey-continuation-line { bottom: 14px; border-top-width: 4px; }
@@ -189,8 +189,8 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
.journey-route { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.journey-rail, .service-info .journey-station .destination-heading, .journey-station, .journey-station-name, .journey-duration, .journey-station::after, .journey-node { transition: none; }
.journey-metro .destination-current, .journey-metro[data-loop=true] .journey-rail { animation: none; }
.journey-station, .journey-continuation, .journey-line, .journey-name-layer, .journey-duration, .service-info .journey-station .destination-heading { transition: none !important; will-change: auto !important; }
.journey-metro .destination-current, .journey-metro[data-loop=true] .journey-rail-window { animation: none; }
}
.weather-info { container-name: weather-card; }
.weather-body { flex: 1 1 0; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; align-items: center; justify-items: center; gap: 24px; }