思考

2026

  • #设计

    Tooltip 的好手感全藏在时间参数里:首次 hover 延迟约 600ms 再出现,鼠标路过不至于满屏弹提示;而一旦有一个 tooltip 开着,相邻元素之间切换就该 0ms 即时跟随——用户已经用行为表达了「我在逐个看说明」。再补一个关闭宽限期,光标跨过按钮之间的空隙时不闪断:

  • #设计

    给悬浮元素加一道 0.5px 的浅灰描边,值回票价的小投入:半透明窗口、截图叠着截图这类场景里,内容深浅不可控,浅色内容贴浅色背景就糊成一片;一条若有若无的发丝线,保证任何背景下都有清晰的边缘。

    半透明窗口上 0.5px 描边带来的边缘对比

    macOS 的窗口、iOS 的卡片都在这么干,网页里却常被忘掉。

  • #设计

    Raycast 的一个细节:连续复制多张图片,图标上会叠一个计数角标。剪贴板在所有系统里都是黑盒——你复制了什么、复制了几个,全靠脑子记;一个小数字就把黑盒开了个窗。

    Raycast 连续复制多张图片时显示的计数角标

    这种细节不会写在需求文档里,只能来自做的人自己较真。功能一点都不新,感知强得要命。

  • #AI#设计

    想让 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 是现成的好实现。

  • #性能#前端

    做曝光埋点,一个卡片挂一个 IntersectionObserver 写起来最顺手,但实测数据是反直觉的:几百个元素共享同一个 observer,明显比一人一个快。回调合并成一次触发、交叉几何的计算在实例内部摊薄,都省在看不见的地方。组件库里值得抽一个 observer 池——对外还是 hook 的形态,对内复用同一个实例。

    参考:IntersectionObserver API Performance: Many vs. Shared

  • #人生

    「有阳光、允许被浪费的下午。」第一次读到这个短语就被击中了。戳人的不是阳光,是「允许」两个字——阳光常有,允许自己白白浪费一下午的时候太少了。上一次什么都不干地晒太阳,已经想不起来是哪天。

  • #工程

    「减少复杂度,简单的才是好的」——架构评审里最常听到、也最经不起追问的一句话。我更信另一种说法:复杂度不灭,它只会转移。业务该有多少内在复杂度就有多少,你能决定的只是它落在哪:压进框架、摊给约定、写进文档、留在代码里,或者最糟的——转嫁给凌晨三点排查问题的人。

    所以评审时真正该问的不是「能不能再简单点」,而是:这坨复杂度打算搬去哪,搬过去之后谁买单。

  • #人生

    读过一个很喜欢的说法:持续不断的爱是不存在的,爱是一个个不连续的微小瞬间——某次并肩走路,某顿一起吃的夜宵,某个刚好接住你情绪的眼神。所谓「持续相爱」,不过是这些瞬间发生得足够频繁,频繁到你愿意把它们连成一条线。

    照这个说法,要操心的就不是激情会不会消退,而是下一个瞬间从哪来。想想压力小多了。

  • #人生

    见过两种傲慢。一种是恃才傲物:他会诚恳地向你论证某个东西为什么好,让你心服口服甚至当场入教——这种骄傲甚至让人更喜欢他,因为底下垫着真本事。另一种是无的放矢:每否定一个大众认可的东西,都只是为了证明自己的选择更特别,被围攻反而成了他的乐趣。

    区别在于:前者的傲慢在抬高某个东西,后者的傲慢只抬高他自己。

  • #工程

    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 对应 HTMLSpanElementvideo 对应 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、defer、async、module 各种加载方式的时间线对比图
    • <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-storeno-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 - xx 为 0 时得到的是正 0,Smi 快路径保得住。

    这属于热点路径才值得碰的微优化,日常照常写 -x 就好。不过知道引擎底下埋着这么多看不见的假设,还挺有意思的。

  • #工程#人生

    Redis 作者 Antirez 有句挺扎心的话:大多数编程既不再是艺术,也算不上高级工程——大多数程序微不足道,写代码的人只需要理解某些接口,然后填一些逻辑和胶水。

    第一次读就破防了,日常工作确实大部分时间在打胶水。不过打胶水也分打得好不好看,缝隙抹得平不平整——能守住的大概也就这点手艺人的自尊了。

2022

  • #设计

    好的错误提示有解剖学。Wix 的设计文档把它拆成五块,一个弹窗里全能对上号:

    • 说清发生了什么——标题直接陈述结果,不是「Error 500」;
    • 说清为什么——「我们这边出了技术问题」,哪怕原因很尴尬;
    • 给一颗定心丸——「你的修改已经保存」,先把用户最怕的事排除掉;
    • 告诉用户怎么修——「请重试」加一个 Try Again 按钮,动作就在手边;
    • 留一条退路——实在不行,联系客服的入口摆在正文里。

    对开发者一样成立:把这五问套在 log 和报错信息上,值班的人会感谢你。

  • #设计

    圆角嵌套的万能公式:内圆角 = 外圆角 − padding。两层圆角矩形要看起来同心,内层的半径必须比外层小掉间距那一截,否则四个角的弧线各弯各的,怎么看怎么别扭。

    圆角嵌套示意:内圆角 = 外圆角 - padding

    从卡片里的缩略图到按钮里的图标底座,全都适用。