优化 Markdown 渲染:从 Next.js MDX 到 React Markdown 的选择之旅

在开发 Next.js 项目时,Markdown 渲染是展示动态内容的重要一环。本周,我们尝试优化项目的 Markdown 渲染体验,计划将现有的 react-markdown 替换为自定义的 NextMarkdown 组件,以提升流式输出效果。然而,测试结果出乎意料,最终让我们决定保留现有方案。这篇文章分享了我们的探索过程和最终决策,希望为类似场景提供参考。

背景:为何要优化?

我们的项目需要渲染流式输出的 Markdown 内容(例如聊天界面),要求内容实时更新且滚动条始终固定在底部显示最新文字。现有方案使用 react-markdown,虽然稳定,但我们希望通过自定义组件进一步提升体验。于是,我们基于 Next.js 官方推荐的 MDX 配置开始了优化之旅。

尝试与挑战

1. Next.js 推荐的 next-mdx-remote

Next.js 文档建议使用 @next/mdxnext-mdx-remote 处理 MDX 内容,这是一个强大的工具,支持在服务器端序列化 Markdown 并在客户端渲染。我们尝试将其集成到自定义的 NextMarkdown 中。

问题

  • 异步延迟next-mdx-remote 依赖异步序列化(serialize),在流式输出场景下,内容更新与渲染之间存在延迟,导致明显的闪烁。
  • 滚动跳动:由于异步渲染,滚动条无法实时跟随底部,破坏了用户体验。

2. 直接使用 dangerouslySetInnerHTML

为了绕过异步问题,我们尝试了 dangerouslySetInnerHTML,直接将解析后的 HTML 注入 DOM。

问题

  • 安全隐患:Next.js 明确警告,这种方式可能导致 XSS 攻击,尤其当内容来源不可控时。
  • 动态渲染不足:它难以处理复杂的动态 Markdown 内容,容易出现不一致性。

解决方案:回归 react-markdown

在对比了多种方案后,我们发现现有 react-markdown 才是最适合的选择。

  • 同步渲染:它直接解析并渲染 Markdown 内容,无需异步序列化,能实时响应流式更新,消除了闪烁问题。

  • 安全可靠 :通过 React 组件渲染,避免了 dangerouslySetInnerHTML 的安全风险。

  • 滚动优化 :结合 useRefuseEffect,我们轻松实现了滚动条固定底部的需求:

    tsx 复制代码
    useEffect(() => {
      if (containerRef.current) {
        containerRef.current.scrollTop = containerRef.current.scrollHeight;
      }
    }, [textContent]);

方案对比

方案 优点 缺点 适用场景
react-markdown 同步渲染、安全、稳定 功能基础,无 MDX 支持 流式输出、动态内容
next-mdx-remote 支持 MDX、静态内容优 异步延迟、闪烁 服务器端渲染、复杂组件
dangerouslySetInnerHTML 实现简单、性能快 安全隐患、动态性差 受控内容、静态渲染

意外收获与总结

本次优化对比测试及结果分析得益于 Grok AI 和 Claude 3.7 的协助。我们通过向 Grok AI 输入流式输出需求和闪烁问题,获取了其对 next-mdx-remote 异步延迟的分析,并得到了同步渲染的建议。Claude 3.7 则帮助对比了 react-markdown 与 dangerouslySetInnerHTML 的优劣,提出滚动管理的代码方案。两者的技术洞察和方案验证加速了问题定位与解决,最终促成保留并优化 react-markdown 的决策。

这次探索让我们意外发现,react-markdown 的同步特性不仅解决了闪烁问题,还简化了动态内容处理,节省了开发成本。虽然 Next.js 的 MDX 方案功能强大,但并非所有场景都适用。最终,我们选择优化现有方案,而不是盲目替换新技术。

对于类似需求的开发者,我的建议是:

  1. 如果需要流式输出,优先考虑同步渲染方案如 react-markdown
  2. 使用 Next.js MDX 时,确保内容更新频率不高,否则可能影响体验。
  3. 始终警惕 dangerouslySetInnerHTML 的安全风险。

接下来,我们将完善 react-markdown 的优化,确保用户体验更上一层楼。你在 Markdown 渲染中遇到过哪些挑战?欢迎分享你的经验!

相关推荐
薛定喵的谔7 天前
我开源了一个精致的 Next.js 博客模板:Skyplume
前端·前端框架·next.js
Flynt7 天前
我的Next.js项目升级到16之后,dev倒是快了,但build差点让我回退
react.js·next.js·turbopack
Patrick_Wilson9 天前
从「改个端口」到 502:Next.js on k8s 的容器端口、Service 映射与 env 覆盖
docker·kubernetes·next.js
疯狂SQL13 天前
手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
typescript·json·next.js·web worker·前端性能优化·esbuild·源码实战
不知疲倦的老鸟13 天前
Node.js 库在浏览器里跑不了的教训
react.js·next.js
倾颜19 天前
从本地 Ollama 到线上多模型 Runtime:接入 DeepSeek / Qwen 的实战复盘
langchain·next.js·deepseek
濮水大叔21 天前
浅论CabloyJS全栈框架提供的“两级页签”机制
typescript·node.js·next.js
IVEN_22 天前
本地正常,Docker 怎么就空白:Next.js SSR 的 Alpine musl DNS 陷阱
前端·docker·next.js
Patrick_Wilson24 天前
K8s 探针避坑:Next.js 不同部署模式下的健康检查实践
kubernetes·node.js·next.js
戈德斯文1 个月前
我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境
react.js·canvas·next.js