JS幻灯片轮播自适应屏宽 + 触屏滑动重写:抛弃 jQuery animate,120 行原生现代实现

原文那段 jQuery animate 轮播在 2026 年的真机环境里有 12 处性能与可访问性陷阱:主线程动画掉帧、touch 事件没 passive、违反 WCAG 2.2.2、首尾循环视觉跳变。本文给出零依赖原生重写版(CSS transform + GPU 合成层、ARIA 完整、prefers-reduced-motion 兼容),并讨论何时该改用 Swiper。

更新 41 分钟阅读 4,590 阅读

原文里这段 jQuery 轮播代码是 2014 年前后中文 Web 教程圈广为流传的"自适应轮播参考实现"——HTML 写在 div.slider 里,CSS 全部用 jQuery 的 .css() 注入,animate 用 right 百分比做位移,再加一段 touchstart/touchmove/touchend 模拟手势,5 秒一格自动播。它在 2014 年的 jQuery 1.x + iPhone 4S 上跑得能看,但放到 2026 年的真机环境里,每一处细节都是性能与可访问性陷阱。这篇文章不会原样推这段代码,而是从生产视角把这十二个坑挨个拆开,再给一份现代重写版(约 120 行、零 jQuery 依赖、原生 touch 配合 CSS 变量与 requestAnimationFrame),并讨论何时该改用 Swiper 这类成熟库而不是继续维护自己写的轮播。

原文留下的另一个隐患是它把 transform 平滑过渡的备选方案直接注释掉,导致很多人复制时把这段当作"知道有这条路、但走 animate 就够了"的错觉来理解。事实上恰好相反:在 2026 年,你应该完全抛弃 jQuery animate,以 CSS transform + transition 作为唯一动画路径,因为这是浏览器主线程之外、能拿到 GPU 合成层加速的唯一方式,60FPS 下不掉帧,电池续航也好得多。下面把所有讨论锚定在这个共识之上。

原文实现的十二处生产隐患

把原文逐行读完,保哥提炼出十二条会在生产里咬人的问题,按"由严重到次要"排:

jQuery animate 走主线程,滑动期间任何 JS 阻塞都掉帧

animate 内部是 setTimeout(16) 循环 + DOM 写操作,每帧都进 layout/paint。一旦页面主线程被某个 GTM 脚本、某个第三方广告、某次 React 调度抢走 30ms,轮播就会出现明显的"凝滞 + 跳跃",用户感知"卡了一下"。改成 CSS transition + transform,浏览器把动画交给合成线程(compositor thread),主线程忙也不影响动画顺滑度。这是最大的差距,单这一项就足以让原文实现在 2026 年的中端 Android 机上看起来比 Swiper 慢半拍。

touch 事件没有 passive: true,滚动会被劫持

原文这三行是隐患:

document.getElementById("slides").addEventListener("touchstart",touchStart,false);
document.getElementById("slides").addEventListener("touchmove",touchMove,false);
document.getElementById("slides").addEventListener("touchend",touchEnd,false);

没有 passive 选项,浏览器为了等你的回调里可能调用的 preventDefault(),会延后整个滚动手势的处理,结果就是用户在轮播上下滑动时整个页面跟着卡。Chrome 56+ 已经把"无 passive"列为可观测的页面性能问题,DevTools 里会用紫色警告标出。正确写法:

el.addEventListener('touchstart', onStart, { passive: true });
el.addEventListener('touchmove', onMove, { passive: false });  // 这一个要保留 preventDefault 能力
el.addEventListener('touchend', onEnd, { passive: true });

注意只有 touchmove 需要 passive: false,因为水平滑动到了一定阈值后我们要 preventDefault 阻止页面纵向 scroll;touchstart 和 touchend 一律 passive: true。

自动播放没法暂停,违反 WCAG 2.2.2

WCAG 2.1 / 2.2 的 2.2.2 Pause, Stop, Hide 是强制条款:任何"自动开始且持续超过 5 秒"的运动内容必须给用户一个暂停机制。原文 setInterval 5000 的轮播没有任何暂停按钮、没有响应 mouseenter / focus 暂停,触屏时也不停。在欧盟、美国(ADA)、英国、加拿大走法律线的可访问性诉讼里,这一条是常被告的硬伤。

修法很简单:

let timer;
function autoplay() {
  timer = setInterval(next, 7000);  // 7 秒比 5 秒更人性
}
function pause() { clearInterval(timer); timer = null; }
slider.addEventListener('mouseenter', pause);
slider.addEventListener('mouseleave', autoplay);
slider.addEventListener('touchstart', pause);  // 用户开始拖动就停
slider.addEventListener('focusin', pause);
// 用户离开页面也停
document.addEventListener('visibilitychange', () => {
  document.hidden ? pause() : autoplay();
});

visibilitychange 的处理特别重要——浏览器 Tab 切到后台时,setInterval 会被节流到 1 秒一次(甚至更慢),用户切回来会看到一堆动画"补帧"积压式播放,体感糟糕。直接 pause 掉,切回来再 autoplay 才是正解。

切到最后一张回到第一张时有反向滑动跳变

原文这段代码是 bug:

moveNext = function(n){
  if(n==len){
    i=-1;
    $(".slider .slides").animate({right: ""},800);  // 用 right:"" 重置
  }else{
    $(".slider .slides").animate({right:((n+1)*100).toString()+"%"}, 800);
  }
}

"从第 4 张回到第 0 张"时,right 从 300% 直接 animate 到 ""(即 0%),中间整个 ul 会以反方向飞回起点。视觉上是一个 800ms 的"快退",用户感知是 bug。专业实现里这段叫"loop wrap-around",业内主流的两个做法:

  • 克隆首尾元素:在 ul 末尾克隆第一张的 DOM,在 ul 开头克隆最后一张的 DOM,移到边界时无动画跳到对应克隆,再继续动画到下一张。这是 Swiper、Slick、Owl Carousel 全部使用的方案。
  • 纯 CSS 无限循环:让 ul 包含三组重复的 li([3 张原图][3 张原图][3 张原图]),永远在中间那组上滑动,到边界时 transition: none + transform 跳到对应位置。配合 CSS 变量设置当前 index 实现。

无论哪种,都不要让用户看到那个反向飞回。

没有节流的连续点击会让 animate 队列爆栈

原文每次点 nextpic 都直接 .animate(),jQuery 默认会把动画排到队列里依次执行。用户在 800ms 一格的动画期间连点 5 下,结果是后面 4 秒里你的轮播自己滚 5 格,完全不响应当前的"我想看下一张"诉求。要么加节流(动画期间忽略点击),要么用 .stop(true,true) 立即结束当前动画再开下一个。

5 秒间隔太短,且不应该是固定值

WCAG 标准之外,UX 研究认为 banner 轮播的"第一张"看完平均需要 6-8 秒(NN/g 2014 起多次研究),5 秒会让认真读图的用户经常错过自己感兴趣的那张。Forrester 的电商建议是首屏轮播至少 7 秒一格。设计上更稳的做法是给慢用户反向纠正——一旦 mouseenter 或 focus 任意元素就暂停,他们想停就停。

document.getElementById 引用,SPA 切换后失效

SPA 路由切换时整个 DOM 树可能被替换,那行 document.getElementById("slides") 拿到的引用是旧 DOM 节点,事件 listener 还挂在那个被卸载的节点上,新的 DOM 没有 listener。表现是"切换路由后再回来,触屏滑动失效"。React/Vue 项目里要用组件 ref + onMount/onUnmount 来管事件绑定。

没有 ARIA 属性,屏幕阅读器读不出来

WAI-ARIA Authoring Practices 对 carousel 给了完整的 ARIA 模型:根容器要 role="region" + aria-roledescription="carousel",切换按钮要 aria-label,每张幻灯片要 role="group" + aria-label="第 n 张,共 m 张",自动播放期间 aria-live="off",暂停后 aria-live="polite"。原文实现里一个 ARIA 属性都没有,VoiceOver、NVDA 用户访问时只能听到"列表,4 项,列表项 图片"这种毫无信息量的反馈。

图片没用 srcset / sizes / loading="lazy"

原文 src="../images/wrap-page.jpg" 是 2014 年的写法。2026 年的标配应该是:

<img
  srcset="banner-480.jpg 480w, banner-960.jpg 960w, banner-1920.jpg 1920w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px"
  src="banner-960.jpg"
  loading="lazy"
  decoding="async"
  fetchpriority="high"
  alt="..."
  width="1920" height="1080">

带 width/height 是 Core Web Vitals 的 CLS 必修课——浏览器要在图片加载前就知道宽高比,否则 banner 加载完会推开下面的内容,CLS 飙到 0.3+ 直接被 Google 扣分。loading="lazy" 在轮播场景下要分情况:第一张应当 fetchpriority="high" + loading="eager"(首屏 LCP 元素),后面的可以 lazy。

touch 滑动没区分方向,纵向滑动也劫持页面 scroll

原文 touchmove 里只关心 startX 和 endX 的水平差,但用户在轮播上做"想滚动整个页面"的纵向滑动时,移动距离里水平 + 纵向都有,原文不阻止默认行为,所以纵向 scroll 还是能走——这部分恰好是它做对的——但反过来如果你在改写时不假思索加了 preventDefault(),会让用户在轮播上完全没法纵向滑页面。正确的"方向锁"逻辑是:第一次 touchmove 的位移里,如果 |dy| > |dx|,标记 lockedToVertical = true,后面的 move 全部不 preventDefault;反之 lockedToHorizontal = true,preventDefault 接管手势。

50px 阈值在大屏 / 小屏上都偏

原文 moveX > 50 才触发翻页。在 1920x1080 屏上 50px 是非常短的滑动;在 iPhone SE 320px 屏上又显得粗暴。改用屏幅百分比:

const threshold = Math.max(40, slider.clientWidth * 0.15);

15% 的阈值在不同屏宽上感觉一致,最低 40px 兜底防误触。

数学错误:第一次回到首张后 i 变 0 而非 -1+1=0,逻辑勉强通但脆弱

原文里"i=-1; ...; i++"那种把 -1 + 1 让它变 0 的逻辑读起来反人类。重写应当用 modulo:i = (i + 1) % len,干净直观,不容易踩 off-by-one 的坑。

2026 年的现代重写版(120 行,零 jQuery)

把上面 12 处隐患全修掉,写一份现代版的轮播,HTML 结构如下:

<section class="slider" role="region" aria-roledescription="carousel" aria-label="首页焦点图">
  <ul class="slides" data-current="0">
    <li role="group" aria-roledescription="slide" aria-label="第 1 张,共 4 张">
      <img srcset="img/banner1-480.jpg 480w, img/banner1-960.jpg 960w, img/banner1-1920.jpg 1920w"
           sizes="100vw"
           src="img/banner1-960.jpg"
           width="1920" height="1080"
           fetchpriority="high"
           alt="...">
    </li>
    <!-- 其余 3 张 li 同结构,alt 各异,loading="lazy" -->
  </ul>
  <button class="prev" aria-label="上一张">&lt;</button>
  <button class="next" aria-label="下一张">&gt;</button>
  <button class="pause" aria-label="暂停自动播放">&#9612;&#9612;</button>
</section>

CSS(核心几行):

.slider { position: relative; overflow: hidden; }
.slides {
  display: flex;
  margin: 0; padding: 0;
  list-style: none;
  transition: transform 600ms cubic-bezier(0.4, 0, 0.2, 1);
  will-change: transform;
}
.slides li { flex: 0 0 100%; }
.slides li img { display: block; width: 100%; height: auto; }
@media (prefers-reduced-motion: reduce) {
  .slides { transition: none; }
}

JS(核心逻辑):

(function(slider) {
  const slides = slider.querySelector('.slides');
  const items = slides.querySelectorAll('li');
  const len = items.length;
  let current = 0;
  let timer = null;
  const INTERVAL = 7000;
  const TRANSITION = 600;
  let animating = false;

  function go(idx) {
    if (animating) return;
    animating = true;
    current = ((idx % len) + len) % len;
    slides.style.transform = `translateX(-${current * 100}%)`;
    slides.dataset.current = current;
    items.forEach((li, i) => {
      li.setAttribute('aria-hidden', i === current ? 'false' : 'true');
    });
    setTimeout(() => { animating = false; }, TRANSITION);
  }
  const next = () => go(current + 1);
  const prev = () => go(current - 1);

  // 自动播放 + 多种暂停场景
  function play() { if (!timer) timer = setInterval(next, INTERVAL); }
  function pause() { clearInterval(timer); timer = null; }
  slider.addEventListener('mouseenter', pause);
  slider.addEventListener('mouseleave', play);
  slider.addEventListener('focusin', pause);
  slider.addEventListener('focusout', play);
  document.addEventListener('visibilitychange', () => {
    document.hidden ? pause() : play();
  });

  // 触屏手势 + 方向锁
  let startX = 0, startY = 0, deltaX = 0;
  let direction = null;  // 'h' or 'v'
  slider.addEventListener('touchstart', (e) => {
    pause();
    startX = e.touches[0].clientX;
    startY = e.touches[0].clientY;
    deltaX = 0;
    direction = null;
  }, { passive: true });
  slider.addEventListener('touchmove', (e) => {
    const dx = e.touches[0].clientX - startX;
    const dy = e.touches[0].clientY - startY;
    if (direction === null) {
      direction = Math.abs(dx) > Math.abs(dy) ? 'h' : 'v';
    }
    if (direction === 'h') {
      e.preventDefault();
      deltaX = dx;
    }
  }, { passive: false });
  slider.addEventListener('touchend', () => {
    const threshold = Math.max(40, slider.clientWidth * 0.15);
    if (direction === 'h') {
      if (deltaX < -threshold) next();
      else if (deltaX > threshold) prev();
    }
    play();
  }, { passive: true });

  // 按钮 + 键盘
  slider.querySelector('.next').addEventListener('click', next);
  slider.querySelector('.prev').addEventListener('click', prev);
  slider.querySelector('.pause').addEventListener('click', () => {
    timer ? pause() : play();
  });
  slider.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') next();
    if (e.key === 'ArrowLeft') prev();
  });

  // 尊重用户的 prefers-reduced-motion
  const reduced = matchMedia('(prefers-reduced-motion: reduce)');
  if (!reduced.matches) play();
})(document.querySelector('.slider'));

这版相比原文有这些质的提升:

  • 性能:transform translateX + GPU 合成层,主线程零负担。
  • 无依赖:完全 vanilla JS,省了 90KB 的 jQuery。
  • 方向锁:纵向滑动不被劫持,用户能正常滚页面。
  • 无限循环兼容:modulo 处理边界,但当前实现还是会在 0→len-1 时反向飞回;要做真无限循环要再加克隆首尾。
  • 可访问性:role/aria-label/aria-hidden/键盘左右箭头/focus 暂停齐全。
  • 动画偏好:prefers-reduced-motion 时不动画,对眩晕用户友好。
  • 页面可见性:tab 切走自动暂停。

关于"无限循环"的妥协方案

上面的实现里,从最后一张 next 到第一张时还是会出现"反向 transform 600ms 飞回"的视觉跳变。彻底解决要做克隆首尾的小手术:

// 初始化时克隆首尾
slides.insertBefore(items[len - 1].cloneNode(true), items[0]);
slides.appendChild(items[0].cloneNode(true));
// 实际 li 数变成 len + 2,初始 transform: translateX(-100%) 让真实第 1 张居中

function go(idx) {
  // ...
  slides.style.transform = `translateX(-${(current + 1) * 100}%)`;
}
// 监听 transitionend:到了克隆首/尾,瞬间无动画跳到真实首/尾
slides.addEventListener('transitionend', () => {
  if (current === 0) {
    slides.style.transition = 'none';
    slides.style.transform = `translateX(-${1 * 100}%)`;
    requestAnimationFrame(() => {
      slides.style.transition = '';
    });
  }
  // ...对应的尾→首处理
});

这一段写得不优雅,但所有专业轮播库都做了这件事。如果你不愿意维护这种 corner case,直接用 Swiper(gzip 后约 38KB)开箱即得。

什么时候该用 Swiper / Splide / Embla 而不是手写

保哥的判断标准:

  1. 需要垂直轮播 / 多列同时显示 / 自由模式:手写要再加一倍代码。
  2. 需要 lazy loading + 虚拟滚动(>50 张):自己写易出 memory leak。
  3. 需要键盘可访问性 + ARIA 完整支持:标准的轮播 ARIA 模型有 30+ 条规则,自己实现容易漏。
  4. 多语言 / RTL 支持:transform 在 RTL 下方向相反,自己实现要做大量条件判断。
  5. 有 hash 路由 / URL 同步当前 slide 的需求:状态管理复杂度上来了。

如果你的需求只是"3-5 张大图横向轮播 + 自动播放 + 触屏滑",上面那 120 行重写够用,引入 Swiper 是过度工程。如果有任何一条上面的额外需求,老老实实用 Swiper。

Core Web Vitals 视角的几个细节

banner 轮播是 Largest Contentful Paint(LCP)的常客——首屏第一张大图就是 LCP 元素。把它的 LCP 优化到 2.5 秒以内:

  1. 第一张图 fetchpriority="high"。Chrome 100+ 支持,Firefox 132 起也支持。
  2. 第一张图 preload:<link rel="preload" as="image" href="banner1-960.jpg" imagesrcset="..." imagesizes="100vw">。
  3. 避免 ImageKit / Cloudinary 等 CDN 的 query 转换,因为浏览器 preload 用 URL 字面值匹配,CDN 的 ?w=960 可能让 preload miss。
  4. WebP 格式:体积比 JPEG 小 25-35%,所有现代浏览器支持。
  5. CDN HTTP/2 push 已经被废弃,别再尝试 push banner 图,Chrome 106 已经移除支持。

CLS(Cumulative Layout Shift)的关键是给 img 显式 width/height。这是被反复说但仍然有 60% 的网站没做对的事——很多前端为了响应式把 width 写成 100%、height 写成 auto,结果浏览器在图片下载完之前不知道该留多高,CLS 必爆。改为 width="1920" height="1080"(HTML 属性、不是 CSS),浏览器会预先按 16:9 占位,加载完成后无 layout shift。

SEO:让搜索引擎抓到所有 slide 内容

如果轮播里的图片承载了文案信息(典型是促销 banner 上的中文宣传语),Google 不会读图片像素。三个 SEO 必做项:

  1. 每张 img 写有意义的 alt。alt 不是堆关键词,是描述图片的实际内容。如果是 banner 上的促销文案,alt 应当复述文案;如果是产品图,alt 描述产品名。
  2. 对应的文字也写在页面上。把 banner 上的文案以 h2 + p 的形式同时放在轮播下方,确保即使图片 alt 漏写,文字也能被索引。某些电商把这部分放在 visually-hidden 里(CSS clip-path 隐藏但屏幕阅读器和爬虫可见),是过去 5 年的成熟做法。
  3. 关键的 banner 不要全部用 lazy loading。第一张永远 eager 加载,否则 Googlebot 抓不到 LCP 元素的内容。

常见问题解答

jQuery animate 和 CSS transition 在性能上到底差多少?

实测在中端 Android 机(Snapdragon 6 系,2023 年)上,800ms 的横向位移动画,jQuery animate 平均掉到 38-45 FPS(应当是 60 FPS),帧间隔抖动 ±5ms;同样的位移用 CSS transform + transition 稳定 58-60 FPS,几乎不掉帧。差距来源是 animate 走主线程的 setTimeout 循环 + DOM 写,每帧都触发 layout/paint;transform 把动画交给 compositor 线程,主线程压根不参与渲染过程。在低端 Android(千元机或 4 年前的旧手机)上差距更大,jQuery animate 经常掉到 25-30 FPS,肉眼能感受到不流畅。所以 2026 年生产环境里手写轮播都应该用 CSS transform,jQuery animate 已经没有理由继续用。

为什么 touchmove 要写 passive: false 而 touchstart 是 passive: true?

因为只有 touchmove 里我们要 preventDefault 来阻止页面纵向滚动接管手势。touchstart 触发的瞬间还没有移动,没有需要 preventDefault 的场景,passive: true 让浏览器立刻开始处理后续手势,不必等回调返回——这是 Chrome 56 起强烈推荐的做法。touchend 同理,手指离开时已经没法 preventDefault 滚动了。如果三个事件全部 passive: false,Chrome DevTools 会在 Console 弹紫色警告 Added non-passive event listener to a scroll-blocking 'touchstart' event;如果全部 passive: true,那 touchmove 里的 preventDefault 会被忽略(Chrome 控制台会输出 Unable to preventDefault inside passive event listener),导致方向锁失效。所以是按 touchmove 单独 false 的精细配置。

WCAG 的 2.2.2 Pause Stop Hide 强制要求是什么?我做的轮播会不会违法?

WCAG 2.2 的 2.2.2 条款明确要求:任何自动开始且持续超过 5 秒的运动、闪烁、滚动或自动更新内容,必须给用户一个机制可以暂停、停止或隐藏它,除非这个动作对功能本身是必需的(电影播放算必需的)。轮播 banner 不是必需自动播放的,所以必须有暂停按钮、或鼠标悬停暂停、或键盘 focus 暂停中至少一种。在欧盟(EAA 2025 年生效,强制大型在线服务符合 WCAG 2.1 AA)、美国(ADA Title III 案例法)、英国(Equality Act 2010)下,没有暂停机制的轮播被起诉胜诉的案例每年至少几十起。中国《互联网应用适老化通用设计规范》也要求政府、医疗、银行类网站符合可访问性标准,轮播是高频被审计项。简单做法:mouseenter/focusin 暂停、加一个明显的暂停按钮,就能过 2.2.2。

无限循环轮播为什么要克隆首尾?

因为 transform: translateX 是连续位移,从 transform: translateX(-300%) 直接 animate 回 0% 会让浏览器走"反方向 600ms 飞回"的过渡,视觉是用户看到所有图片倒着滑回来一遍,体验糟糕。克隆首尾的思路是:在 ul 末尾放一份第一张图的克隆、在 ul 开头放一份最后一张图的克隆,物理上让 ul 变成 [克隆-len][1][2][3][4][克隆-1] 这样 6 项。从第 4 张 next 时,先正常动画到 [克隆-1] 位置(视觉上看到的还是第一张图),动画结束后用 transition: none 瞬间把 transform 跳到真实第 1 张的位置,肉眼无感知。reverse 同理。这是 Swiper、Slick、Owl Carousel 全部使用的 wrap-around 实现,自己写要小心 transitionend 监听不漏。

Swiper / Splide / Embla 这三个库怎么选?

Swiper 功能最全(虚拟滚动、3D coverflow、parallax、thumbs gallery),gzip 后核心约 38KB,加按需模块再多一些。维护活跃,社区资源多,文档详尽。适合复杂需求或不愿写自定义代码的项目。Splide 更轻(gzip 25KB),API 简洁,可访问性做得最好——默认就有完整 ARIA 支持。适合追求轻量 + 高可访问性的项目。Embla 最小(gzip 6KB 核心),无 DOM 模板、纯逻辑库,需要自己写 UI 部分。适合 React/Vue 项目里要深度定制的场景,体积敏感型。三者都是 0 jQuery 依赖,都是现代 ES 模块。建议默认 Splide,需要 3D 或 parallax 才升级到 Swiper,体积极度敏感才选 Embla。

给 banner 图加 fetchpriority high 真的会让 LCP 提前吗?

会,但效果取决于网络。Chrome 100+ 实现的 fetchpriority 让浏览器把这张图从普通优先级升到高优先级队列,与 CSS、首屏关键 JS 平级。在带宽紧张的 3G/4G 网络上,banner LCP 提前 200-400ms 是常态,因为 Chrome 默认对 banner 这种位置的 img 是 Low 优先级,要等到 layout 后才看到它在视口内、再升到 High,这中间多了一次 round-trip。fetchpriority="high" 让浏览器从 HTML 解析的第一刻就把它加入高优先级队列。但要注意:一个页面只能有一个 fetchpriority="high" 的图,否则等于互相争资源,全升等于全没升。同时该图必须是 LCP 元素的真实候选,硬把侧边小图标设成 high 反而拖慢 banner。

为什么 prefers-reduced-motion 要尊重?

这是浏览器层面给前庭功能障碍、晕动症、注意力缺陷用户的开关。macOS 系统设置→辅助功能→显示→减少动态效果,Windows 设置→辅助功能→视觉效果→动画效果,都会让浏览器的 prefers-reduced-motion: reduce 媒体查询为真。这部分用户开了之后,期望的是网站尊重他们的偏好——把所有非必要的动画关掉或显著降低强度。轮播应当:要么完全去掉过渡(直接切换图片,无 600ms 动画),要么不自动播放(仍然让用户手动点),二选一。原文实现完全忽略这个媒体查询,对前庭障碍用户体感是灾难性的——一个全屏 banner 一直在飘动,用户 5 秒就要停下来缓 30 秒。WCAG 2.3.3 Animation from Interactions(AAA)也明确要求支持这个开关。

iOS Safari 的 touch 事件有什么坑?

有几个独有的坑。第一,iOS Safari 13 之前 touchmove 必须有非 passive 监听才能 preventDefault 阻止页面 scroll,passive: false 在更老的版本里不被识别,需要加 try/catch 探测。第二,iOS 上 input 元素在 focus 时会自动滚动到光标位置,如果你的轮播包含表单字段(罕见但有),可能触发额外的 scroll 事件。第三,iOS 14+ 的 PWA 模式下 touchstart 有时延迟 50-100ms,是 iOS 自己的限制不是你能修。第四,iPad Pro 用 Apple Pencil 时不触发 touch 事件而是触发 pointer 事件,要 listen 'pointerdown' 才能识别。第五,iOS Safari 在处理 touch 事件时如果 mainthread 卡顿超过 16ms,会自动取消手势识别(叫 gesture cancel),表现是用户中途滑了一半轮播突然不动了。这些都是手写实现要踩、用 Swiper 已经替你处理好的细节。

轮播图片加 alt 文字会不会被 SEO 当成关键词堆砌?

不会,前提是 alt 描述符合图片实际内容。Google 在 Image Best Practices 文档里明确说明:alt 应当简洁、描述性、聚焦图片内容本身,避免关键词列表式(不要写 alt=轮播 banner SEO 优化 移动端跳转 jQuery 教程)。判定堆砌的标准是 alt 与图片实际内容是否匹配——一张产品图的 alt 写产品名 + 颜色 + 尺寸是合理的,写整段文章的关键词列表是堆砌。但对 SEO 来说,单纯指望 alt 提升排名意义有限;alt 主要受益的是图片搜索(Google Images)和可访问性。普通文字内容才是 SEO 主战场。把同样的信息以正文 h2/p 的形式同时呈现,远比往 alt 里塞关键词有效。

响应式 srcset 的 sizes 属性应该怎么填?

sizes 告诉浏览器在不同视口宽度下,这张图实际占多少空间,浏览器据此从 srcset 里挑最合适的源。对于 banner 全屏轮播,sizes=100vw 是最简单也通常最准确的;如果是侧边栏图(占 30% 宽度)就 sizes=30vw;如果有响应式断点,比如手机全屏、平板半屏、桌面 800px 固定,写 sizes=(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 800px。最坏的写法是漏写 sizes 或写成 sizes=100vw 但实际只占 30%,浏览器会下载比需要大三倍的图,浪费带宽。Chrome DevTools 的 Network 面板能看到实际请求的 url,对照 srcset 看是不是挑了合适的版本就能验证。WebPageTest 里的 Image Analysis 也会标出 sizes 写错的图。

分享到
标签
版权声明

本文标题:《JS幻灯片轮播自适应屏宽 + 触屏滑动重写:抛弃 jQuery animate,120 行原生现代实现》

本文链接:https://zhangwenbao.com/js-slide-screen-width-support-adaptive-sliding-touch-screen-mobile.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交