思考
2026
Tooltip 的好手感全藏在时间参数里:首次 hover 延迟约 600ms 再出现,鼠标路过不至于满屏弹提示;而一旦有一个 tooltip 开着,相邻元素之间切换就该 0ms 即时跟随——用户已经用行为表达了「我在逐个看说明」。再补一个关闭宽限期,光标跨过按钮之间的空隙时不闪断:
给悬浮元素加一道 0.5px 的浅灰描边,值回票价的小投入:半透明窗口、截图叠着截图这类场景里,内容深浅不可控,浅色内容贴浅色背景就糊成一片;一条若有若无的发丝线,保证任何背景下都有清晰的边缘。
macOS 的窗口、iOS 的卡片都在这么干,网页里却常被忘掉。
Raycast 的一个细节:连续复制多张图片,图标上会叠一个计数角标。剪贴板在所有系统里都是黑盒——你复制了什么、复制了几个,全靠脑子记;一个小数字就把黑盒开了个窗。
这种细节不会写在需求文档里,只能来自做的人自己较真。功能一点都不新,感知强得要命。
想让 AI 给你像样的动效和设计,瓶颈常常不在模型,在你的词汇量。会说 stagger、direction-aware、crossfade、layout animation,得到的结果和一句「让它动起来好看点」是两个世界;设计师那些黑话——tighten it up、let it breathe、fix the hierarchy——本质是把审美压缩成了可执行的指令。
以前行话是用来和同行对齐的,现在直接换产出质量,杠杆比从前大多了。Emil Kowalski 整理过一份 motion vocabulary,值得当单词表背。
跟 agent 干活干久了,看什么都像会话管理:人生也需要阶段性地进入 /goal 模式,把一段时间锁死在一个目标上。不然总被冷不丁的 /btw 岔出去——支线任务一岔就是好几年,回过头来才发现主线还挂在那里,进度条没动过。
眼下能做到的大概只有隔三差五问一下自己:现在这是主线,还是支线?
焦点高亮别用 border——border 参与盒模型,聚焦的瞬间元素被挤得动一下,这也是一种布局偏移。用 outline 或 box-shadow 环(Tailwind 里就是
focus:ring-2)画在盒子外面,布局纹丝不动:button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }看到歌手 Mike Posner 讲他的劳力士:七万美元的表,戴出门怕刮花、怕背包拉链划到、怕忘在酒店保险箱里,最后他的结论是——不是我拥有劳力士,是劳力士控制了我。后来他换了块四十美元的天美时,自在了。
我没有劳力士,但买过类似的东西——到手之后一直供着,用一次心疼一次,最后活成了它的保管员。现在下单前会多问一句:这玩意是伺候我的,还是我伺候它的?
凡是会动的数字——倒计时、价格、百分比、实时数据——都应该默认上
tabular-nums。比例数字每个字符宽度不同,数值一跳整行跟着抖:.timer { font-variant-numeric: tabular-nums; }一行 CSS 启用 OpenType 的 tnum 特性,数字变成等宽站桩,怎么跳都不挪窝:
往编辑器里粘贴一条 URL,出来的形态能看出产品想没想过这件事:裸文本是没想;bookmark 大卡片是想了但粗暴,一行引用占掉一屏版面;我最喜欢的是「提及(mention)」——favicon 加标题内联在文字流里,Notion 把它做成了我的肌肉记忆。信息密度不打折,版面不被撑破,Markdown 语义还能无损保留:
一个经典 bug:hover 让元素位移或缩放,光标恰好停在边缘——元素一动就脱离 hover,弹回来又重新命中,无限闪烁。修法是把触发器和效果分离:hover 监听在父元素上,动画作用在子元素上,命中区域自始至终不动:
同类地雷还有不少:hover 放大的卡片、点击后位移的按钮,都是一个思路——让触发区别动,动的是里面的东西。
2025
按钮里的图标随状态切换时,直接跳变最生硬。三档对比:无动画、只过渡 opacity、opacity + scale + blur 三件套——第三档的手感明显最好,模糊把中间帧糅在一起,像光学对焦而不是换幻灯片:
blur 取 4px 这类小值开销很低;实在担心性能,
will-change: filter让浏览器自行决定要不要挪去合成器。图片预览器的完成度,看它怎么对待缩放。大多数实现里缩放只是滚轮手势的副作用,缩到多大、怎么回去全靠感觉;好的预览器把它做成显式状态——加减按钮、当前百分比、一键回正、拖拽平移。缩放从手势的副作用,变成看得见、控得住的状态,用起来的安全感完全不一样:
纯黑或纯白的 logo 直接做 favicon,总有一半用户看不清:浅色标签栏吞白色,深色标签栏吞黑色。SVG favicon 可以内嵌媒体查询,跟着系统主题自动换色:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> <style> path { fill: #1c1c1a; } @media (prefers-color-scheme: dark) { path { fill: #e6e4de; } } </style> <path d="…" /> </svg>浏览器对 SVG favicon 的支持早就够用了,做这个细节的站点却不多。
图标按钮视觉上可以小,热区不能小。想扩大点击区域又不动布局,透明伪元素是最干净的解法:
.icon-button { position: relative; } .icon-button::after { content: ''; position: absolute; inset: -12px; }inset负多少补多少:16px 的图标配-14px,正好凑够 WCAG 建议的 44×44 目标尺寸。手机上尤其值得,指尖没有像素级的准头。
2024
Chrome 114 正式移除了
overflow: overlay,悬浮在内容上、不占布局宽度的滚动条从此没有原生等价物——滚动条重新挤占内容宽度,列表的左右边距不再对称,体验上是一次实打实的降级。替代路线两条:要么接受占位,用
scrollbar-gutter: stable把空间管起来(见滚动条的两个工程细节);要么上自定义滚动条组件,Radix 的 ScrollArea 是现成的好实现。「有阳光、允许被浪费的下午。」第一次读到这个短语就被击中了。戳人的不是阳光,是「允许」两个字——阳光常有,允许自己白白浪费一下午的时候太少了。上一次什么都不干地晒太阳,已经想不起来是哪天。
「减少复杂度,简单的才是好的」——架构评审里最常听到、也最经不起追问的一句话。我更信另一种说法:复杂度不灭,它只会转移。业务该有多少内在复杂度就有多少,你能决定的只是它落在哪:压进框架、摊给约定、写进文档、留在代码里,或者最糟的——转嫁给凌晨三点排查问题的人。
所以评审时真正该问的不是「能不能再简单点」,而是:这坨复杂度打算搬去哪,搬过去之后谁买单。
读过一个很喜欢的说法:持续不断的爱是不存在的,爱是一个个不连续的微小瞬间——某次并肩走路,某顿一起吃的夜宵,某个刚好接住你情绪的眼神。所谓「持续相爱」,不过是这些瞬间发生得足够频繁,频繁到你愿意把它们连成一条线。
照这个说法,要操心的就不是激情会不会消退,而是下一个瞬间从哪来。想想压力小多了。
见过两种傲慢。一种是恃才傲物:他会诚恳地向你论证某个东西为什么好,让你心服口服甚至当场入教——这种骄傲甚至让人更喜欢他,因为底下垫着真本事。另一种是无的放矢:每否定一个大众认可的东西,都只是为了证明自己的选择更特别,被围攻反而成了他的乐趣。
区别在于:前者的傲慢在抬高某个东西,后者的傲慢只抬高他自己。
Rust 新手第一问:为什么
str总是以&str的形态出现?想通一层就通了:str本质是装着合法 UTF-8 的[u8],长度编译期不可知;而 Rust 的变量必须是编译期定长的类型,所以[T]这种不定长值没法直接放进变量。&[T]是胖指针(指针 + 长度),大小固定,于是能上牌桌的是引用而不是本体——&str同理。想通这条,后面
&[T]、Box<str>这些形态也就都顺理成章了。拆解了一个很喜欢的 three.js 球体碎裂效果,学到的全是「用便宜的手段装贵」:
- 没有复杂 shader,景深和光晕全靠后处理;
- 体积光、噪点、烟雾用普通 geometry 模拟,性能好得多;
- 球体碎片是在 3D 工具里预先拆好的 100 多个子 mesh,贴上石头法线,用 Perlin noise 驱动漂浮的 idle 动画。
看完最大的感受:贵的不是技术,是编排。
SWR 里一个漂亮的渲染优化:hook 返回的不是普通对象,而是一组 getter。哪个字段真的被解构、被读取,getter 就把它记进依赖集合;重新验证时只比较被用过的字段,没人读的字段变了也不触发 rerender。
const { data } = useSWR(key)的组件不会因为isValidating的翻转白白重渲染——依赖收集不靠编译器也不靠 Proxy 全家桶,一个 getter 就够。API 形状一点没变,聪明全藏在属性访问这层。React 里给
useRef标类型,与其去背每个元素对应的 DOM 接口名,不如用ElementRef直接从组件反推:import { type ElementRef, useRef } from 'react' // 不用记 HTMLVideoElement 这些名字 const videoRef = useRef<ElementRef<'video'>>(null) // 对第三方组件同样好使 const dialogRef = useRef<ElementRef<typeof Dialog>>(null)span对应HTMLSpanElement、video对应HTMLVideoElement这种映射记不住也不该记——类型系统自己知道。
2023
越想越困难,越做越简单。事情在脑子里多盘一轮,就多长出几个「万一」;上手做掉一步,就实打实少一步。想不通的时候先动手,哪怕只做很小的一块。
刚工作那几年,解决问题给我的多巴胺比刷剧多得多,下班琢磨技术纯粹因为上头。这种状态不怕累,怕的是被人轻飘飘一句「你好卷」——热情成了竞争策略,上头成了表演,解释都没处解释。被扣过几次之后,连上头都变得有点心虚,挺没意思的。
前端圈聊「要不要学 Rust」,最出圈的从来不是技术论证,是两条锐评:一条把 Rust 套进原神开场白——「你说得对,但是 Rust 是由 Mozilla 自主研发的一款全新内存安全编程语言……被引用的指针将被授予生命周期,导引安全之力」;另一条说「学前端不学 Rust,就像四大名著不看红楼梦」。
玩梗归玩梗,梗背后的事是真的:前端工具链的地基正在被 Rust 重写,SWC、Rspack、Turbopack、Oxc、Rolldown。用不用得上另说,看不看得懂日益成为一个问题。
「还能往下滚」的提示箭头,纯 CSS 就能做:给箭头挂一个由
scroll-timeline驱动的透明度动画——滚动条到底,箭头淡出;往回滚,箭头淡入。没有滚动监听,没有节流防抖,状态和滚动位置天生同步:滚动驱动动画这套 API 的意义就在这:过去要 JS 全程陪跑的状态同步,现在声明一次就完事。
见过最优雅的 maxLength 反馈:光标本身是一根进度条。输入 4/10 个字符,光标从底部填充 40% 的蓝色;越接近上限填得越满,右上角还有一个数字计数器兜底。状态就长在你正盯着的地方——打字时视线永远在光标上,反馈跟着视线走,不用扫视角落:
反馈画在视线的落点上,这个思路值得偷。
比小黄鸭调试法更强的是「评论区调试法」:把代码片段发到网上,宣称这是最优解,不服来辩。半小时内就会有人告诉你「换个算法直接干到 nlogn」「某某库里的实现比你这简单十倍」。
互联网上让人帮忙的最快方式,从来不是提问,是给出一个错误答案。
<script>加载方式一图流,值得钉在墙上:- 裸
<script src>:下载 + 执行全程暂停 HTML 解析,性能杀手; defer:并行下载,等解析完再按顺序执行;async:并行下载,下载完立刻执行(此刻暂停解析),顺序不保证;type="module":行为等同 defer,但下载的是整棵依赖树;type="module" async:依赖树并行下载,就绪即执行。
日常选择:默认
defer;统计、埋点这类不依赖别人的脚本用async;裸 script 只留给真正必须阻塞的场景。- 裸
开源最好的报酬不是 star 数,是某天忽然得知:自己写的东西真的帮到了一个具体的人。像一段跌跌撞撞的长跑接近终点时,看台上突然响起的掌声——原来这条路上一直有人看着。
付出让世界变好了一点点,哪怕只有一点点,就够撑很久了。
滚动驱动动画原生化之后,「元素进入视口时淡入」不再需要 IntersectionObserver:
@keyframes enter { from { opacity: 0; transform: translateY(24px); } } .item { animation: enter both; animation-timeline: view(); animation-range: entry 0% entry 100%; }最妙的是天然的 stagger——每个元素的动画进度由它自己和视口的交叉比例驱动,先进来的先播完,错落感不用写一行编排代码:
聚光灯 hover 效果的取巧实现:
background-attachment: fixed把背景钉在视口上,一组卡片共用同一个径向渐变背景,光标移动时只更新一对 CSS 变量,光斑就能无缝跨过卡片边界:.card { background: radial-gradient( circle at var(--x) var(--y), rgba(255, 255, 255, 0.08), transparent ); background-attachment: fixed; }JS 只负责喂坐标,绘制全交给 CSS——分工干净,性能也好。
GitHub README 里的图可以跟随用户的明暗主题切换,用的是标准的
<picture>语法:<picture> <source media="(prefers-color-scheme: dark)" srcset="hero-dark.png" /> <img src="hero-light.png" alt="" /> </picture>深色模式下刺眼的白底大图是开源项目首页的常见破绽,两张图就能补上。同一思路也适用于 favicon 和 OG 图之外的一切「跟着主题走」的静态资源。
人为什么总要等到一个人去世之后,才把最真挚的感谢和评价送出来?悼词里那些话,本人一句也听不到了。
想明白这点之后,我尽量把感谢当场说出口——对朋友、对同事、对写了篇好文章的陌生人。反正夸人又不要钱。
<link rel="prefetch">最阴的坑:它把资源拉进的是 HTTP 缓存。如果这个资源的响应头不可缓存(no-store、no-cache,或者压根没配缓存头),那么等到真正import()的时候,浏览器会把同一个文件再下载一遍——prefetch 白跑,还多付了一次带宽。所以 prefetch 不是「提前拿来放好」的保证,它只是一次低优先级的预热,能不能吃到取决于你的缓存策略配没配对。想给本次导航用的资源提速,那是
preload的活,两者别搞混。scroll-padding不只是给锚点定位用的。给可滚动的输入区加一句scroll-padding-block: 8px,光标移动到边缘触发滚动时,会提前留出一圈呼吸空间,而不是紧贴着容器边缘蹭:一行 CSS 的事,输入的手感立刻精致一档。
轮播图、全屏分页、横向卡片列表,很多人还在用 JS 算滚动位置,其实 CSS 原生的 scroll snap 两行就够:
.carousel { scroll-snap-type: x mandatory; } .carousel > * { scroll-snap-align: center; }滚动结束自动吸附到对齐点,惯性、回弹、触摸手感全是系统级的,比任何 JS 模拟都顺。
mandatory强制吸附,proximity只在靠近时吸附,按场景挑。多行标题最难看的死法:第二行孤零零挂一两个词。
text-wrap: balance让浏览器把几行文字的宽度配平,标题立刻有了排版的样子。Satori(把 HTML/CSS 渲染成图片的库,常用来生成 OG 分享图)也支持了这个属性——分享卡片上的大标题正是最需要配平的地方:级联菜单的老毛病:鼠标从父菜单项沿直线斜着滑向子菜单,中途扫过下面的兄弟项,子菜单啪地换掉了。偷懒的修法是加关闭延迟,更讲究的是「安全三角」——以光标当前位置和子菜单两个角连成三角形,光标在三角形内移动时就认定用户正在奔向子菜单,兄弟项暂时免疫 hover。Amazon 和 Apple 的菜单都这么做,Notion 后来也补上了。
互联网生存指南:效率高,在别人看来就是你很闲。活干得快,换来的不是清闲,是「看起来还有余力」,然后新的活就来了。所以老油条都懂得把效率藏起来,两小时干完的事,挑个体面的时机再交。荒诞,但真实。
一个反直觉的 JS 性能冷知识:
a = 0 - x可能比a = -x快好几倍。原因藏在-0里——当x为 0 时,一元负号产出-0,而-0没法用 V8 的小整数(Smi)表示,类型反馈被污染成浮点,整段热代码跟着去优化;0 - x在x为 0 时得到的是正 0,Smi 快路径保得住。这属于热点路径才值得碰的微优化,日常照常写
-x就好。不过知道引擎底下埋着这么多看不见的假设,还挺有意思的。Redis 作者 Antirez 有句挺扎心的话:大多数编程既不再是艺术,也算不上高级工程——大多数程序微不足道,写代码的人只需要理解某些接口,然后填一些逻辑和胶水。
第一次读就破防了,日常工作确实大部分时间在打胶水。不过打胶水也分打得好不好看,缝隙抹得平不平整——能守住的大概也就这点手艺人的自尊了。
2022
好的错误提示有解剖学。Wix 的设计文档把它拆成五块,一个弹窗里全能对上号:
- 说清发生了什么——标题直接陈述结果,不是「Error 500」;
- 说清为什么——「我们这边出了技术问题」,哪怕原因很尴尬;
- 给一颗定心丸——「你的修改已经保存」,先把用户最怕的事排除掉;
- 告诉用户怎么修——「请重试」加一个 Try Again 按钮,动作就在手边;
- 留一条退路——实在不行,联系客服的入口摆在正文里。
对开发者一样成立:把这五问套在 log 和报错信息上,值班的人会感谢你。
圆角嵌套的万能公式:内圆角 = 外圆角 − padding。两层圆角矩形要看起来同心,内层的半径必须比外层小掉间距那一截,否则四个角的弧线各弯各的,怎么看怎么别扭。
从卡片里的缩略图到按钮里的图标底座,全都适用。