Repository navigation
Kineto v0.12.2
Kineto v0.12.2
English
-
First published 0.12 release.
v0.12.0andv0.12.1were tagged, but neither release run published anything — 0.12.0 stopped on a real Tabs bug, and for 0.12.1 CI passed while the release's identical re-run failed on one flaky browser test — so npm stayed on 0.11.0. Everything listed under 0.12.1 and 0.12.0 ships in this version, and the release workflow no longer re-runs CI (below). -
Make CI and releases faster without dropping a gate. Every lane —
test:node,test:browser,test:browser:cross— runs throughscripts/run-lane.mjsstraight from itspackage.jsonlist: a failing test is retried on its own instead of re-running the whole lane (one flake used to cost about 7 minutes per retry), every failing test is reported in one pass with its time, and--shard k/nsplits a lane across runners. CI runs one Node job and five browser shards (Chromium ×2, WebKit ×2, Firefox) at the same time, so it takes as long as its slowest shard (about 6–10 minutes) instead of about 25. The workflows no longer keep their own copies of the test lists; the Firefox/WebKit copy had drifted fromnpm run test:browser:cross, which now runs the same 28 files as CI. -
The release workflow trusts CI instead of repeating it. It requires CI's green result for the tagged commit on
main(scripts/require-green-ci.mjs, with the job's read-only token), then checks and packs the package —test:release-package, a Chromium smoke of every module from that build, the lockfile audit — in about 5 minutes instead of 25. For v0.12.1, CI passed and the identical re-run inside the release failed on one flaky test, burning the version; now a red CI can be re-run and the release re-run with the same tag, because nothing was published. -
Flaky tests are reported, never hidden: a test that passes only on a retry prints
Flaky:, leaves a CI warning annotation and is listed in the lane summary, and a test that times out on every attempt says so in an error annotation (a killed process cannot report for itself). Hunt a flake locally withnode scripts/run-lane.mjs test:browser --only <name> --repeat 3, which turns retries off. -
New
npm run verify:push— lint, build with a check for stale generated files, every Node test, demo QA (about 4 minutes) — is the required check before any push;npm run hooks:installadds an opt-in pre-push hook that runs its fast form. 15 of the last 26 red CI runs failed on something it catches.AGENTS.mdnow states the push rules for every agent. A local build stamps the demo's build id asdev(CI, which builds what Pages deploys, stamps the commit), so the committedsite/index.htmlno longer changes on every commit. -
Demo: the Flip block no longer leaves a hole. Its two cards (FLIP grid and Fold) were
.widein a 3-up grid, so the first took two thirds of the row and the second a full row below it; they now sit side by side, and every row that cannot be completed is split evenly (not only the last one —planShortRowsindemo/main.js). The opened Fold tiles shrink with the stage instead of spilling out of a half-width card or a phone. Gate:tests/browser/demo-blocks.mjs(no row holes at 1440 and 1024 px). -
Demo: a block no longer ends on one stretched card after a row of three. The last two rows share their cards instead (Date Time 3+1 → 2+2; Overflow Text, Glitch and Stylize end on two pairs). After a row of two a lone card still fills its row, and deliberate full-row demos (
.card.full) keep theirs. Gate:tests/browser/demo-blocks.mjs. -
Demo: rebalancing a grid no longer trips WebKit's "ResizeObserver loop completed with undelivered notifications" page error. The observer rebalanced inside its own callback, which changed the grid's height and re-notified it in the same frame; it now reacts only to width changes and rebalances on the next frame. The CSS Scroll check in
tests/browser/demo-blocks.mjsholds Lenis still while it scrolls by hand and starts from a card that is really in view (CI read the view timeline as 0 before and after), and its failure message now carries the scroll position, the card's box and its animations. -
New
Kineto.config({ defer: true })(off by default): effects found byscan()are created only when their element comes within one viewport of the screen, for modules that declaredefer: true— the visual effects (Reveal, the text modules, Glitch, Stylize, Squircle, Canvas Effect, Card Glow, Tilt, Magnetic, Mouse Parallax, Parallax, CSS Scroll, Scroll Velocity, Scroll Shadows, Cover Reveal, Brush Reveal, Counter, Marquee, Loading Indicator, Ripple, Vibrate, Confetti). Pins, page-level modules, components with keyboard/ARIA behaviour, Lazy and Ambient Media are still created at once, direct calls such asKineto.reveal(el)are never deferred, options are read when the instance is finally created, anddestroy()cancels queued work. Custom modules opt in withdefer: truein their definition (typed onKinetoModule). Gate:tests/browser/deferred-create.mjs. -
Overflow Text rebuilds after a width change at the next frame instead of inside its ResizeObserver callback. Rebuilding changes the element's own box, and doing it in the callback re-notified the observer in the same frame: WebKit reported "ResizeObserver loop completed with undelivered notifications" as a page error on the demo after a viewport resize.
-
Lazy and Stylize measure their media wrapper in one batch at the end of the creating task (
measureThenApplyThisTaskinsrc/utils.js) instead of reading style and layout between each image's DOM writes: 40 images created together now force 1 layout instead of 40, and the wrapper still has its box before the first paint (tests/browser/create-cost.mjs). -
Demo: much faster on phones. Module blocks far from the viewport skip rendering (
content-visibility: auto; blocks that pin keep rendering), rows and folds are measured only for blocks near the viewport, touch devices create effects as they come near (defer), and the ~240 playground titles attach their Overflow Text when their card nears. Emulated phone (iPhone 13 viewport, 4× CPU throttle): load event 7.5 → 2.7 s, long tasks in the first 10 s 9.8 → 3.4 s, instances right after load 520 → 102, main thread at rest 100% → about 30% busy; scrolling 12 screens at a steady speed ran at 11 fps (median frame 83 ms, 62 frames over 50 ms) and now at 39 fps (median 17 ms, 16 over 50 ms). -
Demo: the page never shifts under the reader, however they got there. A skipped block is drawn at its real height only when it nears, so the demo anchors the scroll itself (the browser's anchoring is off there, and iOS has none): a block above the screen that changes height moves the page with it, before paint. It waits while another script's smooth scroll runs and while a touch fling coasts (moving the page would stop either); for the fling case, blocks above the screen that were never drawn are drawn once while the page rests. The side nav,
#hashlinks, the compare sheet and share links jump through oneKINETO_BLOCKS.jumpTo(), whose glide re-aims every frame and arrives a screen short at once when the target is far — it lands exactly without measuring the blocks in between first (that measuring held a phone for a third of a second). -
Demo: a deep link such as
/#mod-countercould open a different module. While the intro held the page still, blocks drawn under it moved, the scroll spy rewrote the address to whichever block was then in view, and the jump after the intro followed the rewritten address. The spies now wait until a deep link's jump has settled, and keeping the active side-nav link visible scrolls only the nav itself —scrollIntoView()had also pulled the locked page to the top (tests/browser/b2_navigation.mjs). -
tests/browser/stylize.mjsopens a card's fold before pressing its motion switch: a card past a block's first two rows is inert behind "Show more demos", and the v0.12.2 CI run failed on it in all three engines after the demo's last rows were rebalanced. Browser tests that read a far grid or count instances around the Loader demo now bring it near first, the way a reader does (demo-polish.mjs,measure.mjs), and tests that go to a far demo use the demo's ownKINETO_BLOCKS.jumpTo(): Playwright'sscrollIntoViewIfNeeded()does not move WebKit to an element inside a block that skips rendering.
한국어
-
처음 배포되는 0.12 릴리스입니다.
v0.12.0·v0.12.1태그는 만들어졌지만 두 릴리스 모두 아무것도 게시하지 못했습니다 — 0.12.0은 실제 Tabs 버그에서 멈췄고, 0.12.1은 CI가 통과한 뒤 릴리스가 같은 검사를 다시 돌리다 흔들리는 브라우저 테스트 하나에 실패했습니다. 그래서 npm은 0.11.0에 머물렀습니다. 0.12.1과 0.12.0에 적힌 내용은 모두 이 버전에 들어 있고, 릴리스 workflow는 더 이상 CI를 다시 돌리지 않습니다(아래). -
검사를 빼지 않고 CI와 릴리스를 빠르게 했습니다. 모든 레인(
test:node·test:browser·test:browser:cross)이package.json목록 그대로scripts/run-lane.mjs로 돕니다. 실패한 테스트만 따로 재시도하고(전에는 테스트 하나가 흔들리면 레인 전체를 다시 돌려 재시도마다 약 7분), 실패한 테스트를 한 번에 모두 시간과 함께 보고하며,--shard k/n으로 레인을 여러 러너에 나눕니다. CI는 Node 작업 하나와 브라우저 shard 다섯 개(Chromium 2·WebKit 2·Firefox 1)를 동시에 돌려, 약 25분 걸리던 시간이 가장 느린 shard 하나(약 6~10분)로 줄었습니다. workflow에 테스트 목록 사본을 따로 두지 않습니다. Firefox/WebKit 사본은npm run test:browser:cross와 이미 어긋나 있었고, 이제 이 명령이 CI와 같은 28개 파일을 돌립니다. -
릴리스 workflow가 CI를 반복하지 않고 CI 결과를 믿습니다. 태그가 가리키는
maincommit의 CI 성공을 확인한 뒤(scripts/require-green-ci.mjs, 작업의 읽기 전용 토큰) 패키지만 검사해 묶습니다(test:release-package, 그 빌드로 모든 모듈을 돌리는 Chromium smoke, lockfile 감사). 25분 걸리던 릴리스가 약 5분이 됩니다. v0.12.1은 CI가 통과한 commit을 릴리스가 똑같이 다시 돌리다 흔들리는 테스트 하나에 실패해 버전을 잃었습니다. 이제는 아무것도 배포되지 않았으므로, 실패한 CI를 다시 돌린 뒤 같은 태그로 릴리스를 다시 돌리면 됩니다. -
흔들리는 테스트를 숨기지 않고 알립니다. 재시도에서야 통과한 테스트는
Flaky:를 출력하고 CI warning annotation을 남기며 레인 요약에 나옵니다. 모든 시도에서 시간 초과된 테스트는 error annotation으로 그 사실을 남깁니다(강제 종료된 프로세스는 스스로 보고하지 못함). 로컬에서는 재시도를 끈node scripts/run-lane.mjs test:browser --only <이름> --repeat 3으로 찾습니다. -
새
npm run verify:push(lint, 생성 파일이 낡았는지 확인하는 build, 모든 Node 테스트, 데모 QA — 약 4분)를 모든 push 전에 반드시 돌립니다.npm run hooks:install은 그 빠른 형태를 도는 선택형 pre-push hook을 설치합니다. 최근 실패한 CI 26건 중 15건이 이 명령으로 잡히는 문제였습니다.AGENTS.md에 모든 에이전트가 따를 push 규칙을 적었습니다. 로컬 빌드는 데모 build id를dev로 찍고 커밋 id는 Pages가 배포할 빌드를 만드는 CI만 찍으므로, 커밋된site/index.html이 커밋마다 바뀌지 않습니다. -
데모: Flip 블록에 빈자리가 생기지 않습니다. 두 카드(FLIP 그리드·Fold)가 3열 그리드의
.wide라 첫 카드는 줄의 2/3, 둘째 카드는 그 아래 한 줄 전체를 차지했습니다. 이제 두 카드가 나란히 놓이고, 채울 수 없는 줄은 마지막 줄뿐 아니라 어느 줄이든 균등하게 나눕니다(demo/main.js의planShortRows). 펼친 Fold 타일은 반폭 카드나 휴대폰에서 스테이지 밖으로 넘치지 않고 함께 줄어듭니다. 게이트:tests/browser/demo-blocks.mjs(1440·1024px에서 줄 빈자리 없음). -
데모: 세 개짜리 줄 뒤에서 블록이 늘어난 카드 하나로 끝나지 않습니다. 마지막 두 줄이 카드를 나눠 가집니다(Date Time 3+1 → 2+2, Overflow Text·Glitch·Stylize는 두 개씩 두 줄). 두 개짜리 줄 뒤에 홀로 남은 카드는 전처럼 한 줄을 채우고, 의도적으로 한 줄을 다 쓰는 데모(
.card.full)도 그대로입니다. 게이트:tests/browser/demo-blocks.mjs. -
데모: 그리드 재배치가 WebKit의 "ResizeObserver loop completed with undelivered notifications" 페이지 오류를 더 내지 않습니다. 관찰자가 자기 콜백 안에서 재배치해 그리드 높이가 바뀌고 같은 프레임에 다시 통지됐습니다. 이제 폭 변화에만 반응하고 다음 프레임에 재배치합니다.
tests/browser/demo-blocks.mjs의 CSS Scroll 검사는 손으로 스크롤하는 동안 Lenis를 멈추고, 실제로 화면 안에 있는 카드에서 시작합니다(CI에서 view 타임라인이 전후 모두 0). 실패 메시지에 스크롤 위치·카드 위치·애니메이션 상태를 담습니다. -
새
Kineto.config({ defer: true })(기본 꺼짐):scan()이 찾은 효과 중 모듈이defer: true를 선언한 것은 요소가 화면 한 칸 거리 안에 들어올 때 만듭니다 — 시각 효과들(Reveal, 텍스트 모듈, Glitch, Stylize, Squircle, Canvas Effect, Card Glow, Tilt, Magnetic, Mouse Parallax, Parallax, CSS Scroll, Scroll Velocity, Scroll Shadows, Cover Reveal, Brush Reveal, Counter, Marquee, Loading Indicator, Ripple, Vibrate, Confetti). 핀·페이지 전체 모듈·키보드/ARIA가 있는 컴포넌트·Lazy·Ambient Media는 전처럼 바로 만들고,Kineto.reveal(el)같은 직접 호출은 기다리지 않으며, 옵션은 실제로 만드는 순간의 마크업에서 읽고,destroy()는 기다리는 작업도 지웁니다. 직접 만든 모듈은 정의에defer: true를 두면 참여합니다(KinetoModule타입). 게이트:tests/browser/deferred-create.mjs. -
Overflow Text는 너비가 바뀌면 ResizeObserver 콜백 안이 아니라 다음 프레임에 다시 만듭니다. 다시 만들면 요소 자신의 크기가 바뀌어 같은 프레임에 관찰자가 또 불렸고, WebKit은 데모에서 화면 크기를 바꾼 뒤 "ResizeObserver loop completed with undelivered notifications"를 페이지 오류로 보고했습니다.
-
Lazy·Stylize가 미디어 래퍼 크기를 이미지마다 DOM을 쓰는 사이에 스타일·레이아웃을 읽는 대신, 만드는 작업이 끝날 때 한 번에 모아 잽니다(
src/utils.js의measureThenApplyThisTask). 이미지 40개를 함께 만들 때 강제 레이아웃이 40번에서 1번이 되고, 래퍼는 첫 페인트 전에 크기를 갖습니다(tests/browser/create-cost.mjs). -
데모: 휴대폰에서 훨씬 빨라졌습니다. 화면에서 먼 모듈 블록은 렌더링을 건너뛰고(
content-visibility: auto, 핀이 있는 블록은 제외), 줄 배치·접기 측정은 화면 근처 블록만 하며, 터치 기기는 효과를 가까워질 때 만들고(defer), 설정 제목 약 240개의 Overflow Text도 카드가 가까워질 때 붙입니다. 휴대폰 에뮬레이션(iPhone 13 화면, CPU 4배 감속): 로드 이벤트 7.5 → 2.7초, 첫 10초의 긴 작업 9.8 → 3.4초, 로드 직후 인스턴스 520 → 102개, 가만히 있을 때 메인 스레드 사용 100% → 약 30%. 12화면을 일정한 속도로 스크롤하면 11fps(프레임 중앙값 83ms, 50ms 넘는 프레임 62개)였던 것이 39fps(중앙값 17ms, 16개)가 됐습니다. -
데모: 어떤 방법으로 이동해도 읽던 화면이 밀리지 않습니다. 건너뛴 블록은 가까워질 때에야 실제 높이로 그려지므로, 데모가 스크롤 앵커링을 직접 합니다(데모는 브라우저 앵커링을 꺼 두었고 iOS에는 아예 없음). 화면 위쪽 블록의 높이가 바뀌면 페인트 전에 그만큼 페이지를 옮깁니다. 다른 스크립트의 부드러운 스크롤이 도는 동안과 터치 플링이 관성으로 흐르는 동안은 기다리고(페이지를 옮기면 둘 다 멈춤), 플링을 위해 한 번도 그려지지 않은 화면 위쪽 블록은 페이지가 멈춰 있을 때 한 번씩 미리 그려 둡니다. 사이드 내비·
#hash링크·비교 시트·공유 링크는 모두KINETO_BLOCKS.jumpTo()하나로 이동하고, 그 글라이드는 매 프레임 목표를 다시 읽으며 먼 목표면 한 화면 앞까지 먼저 건너뜁니다 — 지나가는 블록을 미리 재지 않고도 정확히 닿습니다(그 측정이 휴대폰을 0.3초 붙잡았음). -
데모:
/#mod-counter같은 딥 링크가 다른 모듈을 열 수 있었습니다. 인트로가 페이지를 잡아 둔 동안 그 아래에서 블록이 그려지며 위치가 바뀌자, 스크롤 스파이가 그때 보이던 블록으로 주소를 바꿨고, 인트로 뒤의 이동이 바뀐 주소를 따라갔습니다. 이제 스파이는 딥 링크 이동이 자리 잡을 때까지 기다리고, 사이드 내비의 활성 링크를 보이게 할 때는 내비 자체만 스크롤합니다 —scrollIntoView()가 잠긴 페이지까지 맨 위로 끌어올렸습니다(tests/browser/b2_navigation.mjs). -
tests/browser/stylize.mjs가 모션 스위치를 누르기 전에 카드의 접기를 엽니다. 블록 첫 두 줄 뒤의 카드는 "데모 더 보기" 뒤에서 inert이고, 데모 마지막 줄 재배치 뒤 v0.12.2 CI가 세 엔진 모두 이것으로 실패했습니다. 먼 그리드를 읽거나 Loader 데모 주변의 인스턴스를 세는 브라우저 테스트도 독자처럼 먼저 그곳으로 가고(demo-polish.mjs,measure.mjs), 먼 데모로 가는 테스트는 데모 자신의KINETO_BLOCKS.jumpTo()를 씁니다 — Playwright의scrollIntoViewIfNeeded()는 렌더링을 건너뛰는 블록 안의 요소로 WebKit을 옮기지 못합니다.