折叠屏适配实战从布局到性能的全面优化策略

设计师英洁 移动 阅读 2,106
赞 21 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

说实话,这个折叠屏项目上线前我真没觉得会出大问题。功能都实现了,UI 也适配了展开和折叠状态,结果一测性能,直接傻眼。在 Fold 3 上打开页面,手指滑动列表,帧率掉到 20 多,有时候甚至卡住半秒才响应。用户反馈说“像在用五年前的手机”,这谁顶得住?

折叠屏适配实战从布局到性能的全面优化策略

最离谱的是,页面刚展开的时候,整个界面要等 1.5 秒才“活过来”,按钮点不动,滚动也冻结。我一开始以为是资源加载慢,但静态资源总共不到 300KB,根本不是网络的问题。

找到病根了!

我先上了 Chrome DevTools 的 Performance 面板录了一段操作,拖动、点击、展开设备,全来一遍。一看火焰图,好家伙,resize 事件触发了上百次,每次都在重新计算布局、重绘 DOM,还顺带跑了一遍媒体查询匹配逻辑。

再看 Memory 面板,每次展开/折叠,内存瞬间涨 40MB,GC 频繁触发,主线程直接被堵死。结合 User Timing API 打点,发现从 window.onresize 触发到页面可交互,平均耗时 1200ms,峰值能到 1800ms。

定位到了:问题不在数据量,也不在接口,而是**折叠屏频繁的尺寸变化导致的高频重排重绘 + 无节制的响应式逻辑执行**。

三种方案试了一遍

我试了三种主流思路:

  • 第一种:节流 resize 事件,300ms 节流一次。结果——视觉延迟明显,展开后内容半天不更新,体验更差。
  • 第二种:用 CSS 容器查询(@container)替代部分媒体查询。想法很好,但兼容性太差,目标机型系统版本不支持,pass。
  • 第三种:监听 window.visualViewport 和硬件 screen.orientation 变化,配合防抖 + 条件渲染。这个最终成了。

核心思路是:别一 resize 就干活,先判断是不是真的“形态切换”——是从折叠变展开,还是只是键盘弹起这种小波动。

核心代码就这几行

关键是在 resize 回调里加了个“形态检测”逻辑:

let lastState = {
  width: window.innerWidth,
  height: window.innerHeight,
  isExpanded: false
};

const EXPANDED_THRESHOLD = 800; // 屏幕宽度大于800认为是展开态

function handleResize() {
  const currentWidth = window.innerWidth;
  const currentHeight = window.innerHeight;

  // 判断是否超过阈值,且与上一次状态不同
  const shouldUpdate = Math.abs(currentWidth - lastState.width) > 50 ||
                       Math.abs(currentHeight - lastState.height) > 50;

  if (!shouldUpdate) return;

  const isNowExpanded = currentWidth > EXPANDED_THRESHOLD;
  
  if (isNowExpanded !== lastState.isExpanded) {
    lastState = {
      width: currentWidth,
      height: currentHeight,
      isExpanded: isNowExpanded
    };

    // 只在这个时机触发昂贵的 UI 更新
    debounce(updateExpensiveUI, 100)();
  } else {
    lastState.width = currentWidth;
    lastState.height = currentHeight;
  }
}

function updateExpensiveUI() {
  document.body.classList.toggle('expanded', lastState.isExpanded);
  // 这里可以触发 React 组件重渲染,或切换布局结构
  renderLayout(lastState.isExpanded);
}

window.addEventListener('resize', handleResize);

另外,原本我们在 CSS 里用了七八个 (max-width)(min-height) 的媒体查询组合,全都干掉,换成只监听 body 上的类:

/* 原来这样写,每一项都会触发重排 */
@media (min-width: 800px) { .sidebar { display: block; } }
@media (max-width: 799px) { .sidebar { display: none; } }

/* 改成这样,只依赖一个 class */
.expanded .sidebar {
  display: block;
  flex-shrink: 0;
  width: 280px;
}

.sidebar {
  display: none;
  transition: width 0.3s ease;
}

这里注意我踩过好几次坑

第一次改完,我发现三星 Fold 的键盘弹出也会触发 resize,高度从 700px 掉到 500px,结果误判成“折叠态”,菜单直接没了。后来加上了防抖 + 高度变化容忍阈值,才稳住。

还有一次是用了 requestAnimationFrame 包一层,结果某些低端机上 rAF 本身就被卡住,反而更慢。最后换回 setTimeout + 100ms 防抖,简单但稳定。

另一个坑是 innerWidth 在 Android WebView 里偶尔不准。后来补了降级逻辑,优先用 visualViewport.width

const viewportWidth = window.visualViewport ? window.visualViewport.width : window.innerWidth;

优化后:流畅多了

改完之后再测,效果立竿见影:

  • 从形态变化到 UI 响应,平均时间从 1200ms 降到 110ms
  • 滚动帧率稳定在 55-60fps,偶有掉帧也不低于 45
  • 内存占用减少 35%,GC 次数下降 70%
  • 用户反馈“终于不卡了”,内部测试通过

最关键的是,原来展开后要等 1.5 秒才能点按钮,现在基本是秒切,肉眼几乎看不出延迟。

性能数据对比

这是上线前后的真实数据(取自 Sentry + 自研埋点):

指标 优化前 优化后
首屏可交互时间(展开态) 1480ms 820ms
resize 到 UI 更新延迟 1200ms 110ms
滚动平均帧率 28fps 58fps
内存峰值增长(单次展开) +42MB +12MB

别看只是改了几行 JS + 换了个 CSS 写法,实际影响巨大。尤其是对中低端折叠屏设备,省下来的每一毫秒都是用户体验。

还有点小问题,但不影响大局

目前还有一个边缘情况:快速连续折叠/展开两次,偶尔会出现一次漏更新。排查发现是防抖时间设得太短,延长到 150ms 后缓解,但稍微增加了一点延迟。权衡之下选择保留 100ms,毕竟没人会连续折展开玩。

另外,这个方案依赖 JavaScript,如果用户关了 JS,那就没办法了。不过我们项目是 SPA,JS 是刚需,所以不考虑降级。

以上是我踩坑后的总结,希望对你有帮助

折叠屏的性能问题不像普通移动端那么直观,它藏在那些“你以为正常的 resize”背后。别迷信媒体查询,也别一上来就搞复杂框架适配,先看看是不是自己在频繁触发重排。

这个方案不是最优的,比如未来可以尝试用 Intersection Observer 监听虚拟容器,或者上 Web Components 做局部更新。但现在这套够用、稳定、兼容性好,适合大多数团队落地。

以上是我个人对这个折叠屏性能优化的完整讲解,有更优的实现方式欢迎评论区交流。这类实战我后续还会继续分享。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论