前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >spa 如何达到ssr 的秒开技术方案——预渲染

spa 如何达到ssr 的秒开技术方案——预渲染

作者头像
winty
发布2023-08-23 10:17:39
4690
发布2023-08-23 10:17:39
举报
文章被收录于专栏:前端Q

预渲染

SPA(单页应用)在初次加载时,由于需要加载所有必要的 JavaScript 和 CSS 文件,以及应用的主 HTML 文件,因此可能会产生白屏时间较长的问题,对用户体验而言是非常糟糕的。

其中白屏时间主要影响因素之一:SPA 应用在加载完成后,需要再进行一次 DOM 渲染才能显示页面内容。在渲染过程中,可能需要加载大量的 JavaScript 文件、CSS 文件或网络请求,这些操作都需要耗费时间,从而导致白屏时间变长。

对单页面应用进行预渲染,将页面在打包期间渲染成静态 HTML 文件,可以很好的解决白屏时间过长问题

预渲染的几个优势:

  1. 优化 SEO

由于单页面应用通常只有一个入口 HTML 文件,因此其页面内容无法被搜素引擎爬虫捕获到。而使用预渲染功能,可以让项目构建出包含所有动态内容的静态 HTML 页面,从而被搜索引擎爬虫作为内容来源,提高 SEO 优化效果。

  1. 更快的加载速度

使用预渲染功能,可以将动态生成的部分预先生成静态文件,无需等到页面加载完成后再生成,从而提高网站的加载速度。

  1. 更好的用户体验

预渲染后,用户进入网站时可以更快地获取到内容,可以提高用户的体验。

  1. 减轻服务端压力

使用静态资源替代计算资源,可以减轻服务端的压力。预渲染后的页面不需要借助服务器的计算资源,减轻了服务器的压力,提高了页面处理效率。

核心流程

社区也提供了prerender-spa-plugin 这类插件,可以直接集成到项目中使用,由于得物预发、正式环境静态资源都是应用cdn的,会导致预渲染异常。本地启动服务,cdn上无对应资源。最终由团队内手动实现一款具备相同功能的static-generator插件

核心流程

接下来通过代码简单的看一下其各个环节是如何做的

首先需要做的,定义一个gererate函数和一个Renderer类

代码语言:javascript
复制
const generate = () => {}

class Renderer {}

gererate 主要是用于处理参数和流程处理

Renderer 主要是用于启动无头浏览器生成HTML

首先看一下Renderer是如何生产HTML的:

核心是使用puppeteer

Puppeteer 是一个由 Google 推出的 Node.js 库,它提供了一个高级 API ,可以使用 Headless Chrome 或 Chromium 来控制 Chrome 或 Chromium 的行为,用于测试、屏幕截图和数据爬取等。 Puppeteer 可以模拟人类的操作,比如点击、填充表单、下拉、切换页面、截图等,同时还可以拦截网络请求和处理 Cookies 等功能。由于其灵活性和易用性,许多开发者使用它进行爬虫、测试、数据分析等任务。

相关代码:利用puppeteer 启动一个无头浏览器获取页面的HTML

代码语言:javascript
复制
const getHtml = async ({ userAgent, onRequest, url }) => {
  const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }) // 启动无头浏览器

  const page = await browser.newPage()
  await page.setViewport({ width: 375, height: 812 })
  if (userAgent) await page.setUserAgent(userAgent)
  // 页面实例上下文中执行的方法
  await page.evaluateOnNewDocument(() => (window['_prerender_'] = 'prerender'))
  // 启用请求拦截器
  await page.setRequestInterception(true)
  // 监听请求
  page.on('request', (request) => onRequest?.({ request, pageUrl: url, page }))

  await page.goto(url)
  // 页面实例上下文中执行的方法
  await page.evaluate(observeRender) // 默认5000ms页面加载完成
  const content = await page.content()
  await page.close()
  return content
}

上述代码除了生成HTML外,还有一个十分重要的事情 page.on('request',(request) => onRequest),其拦截了页面的所有请求,将所有的CSS资源进行了缓存

onRequest实现

代码语言:javascript
复制
const cssContent = {}

const onRequest = asycn ({ request }) => {
  const body = await read(url)
  if (cssInline && filename.endsWith('.css')) {
    const byteLength = body.byteLength
    const { maxSize = Infinity, exclude, include } = cssInline

    if (!(byteLength > maxSize || exclude?.test(filename)) && (!include || include.test(filename))) {
      // 把css 先存起来
      cssContent[filename] = body.toString()
    }
  }
  
  return request.respond({
    status: 200,
    body,
  })
}

其核心就做了两个事情

  1. 获取css文件的内容
  2. 缓存到cssContent中,后面生成html时使用

至此已经可以获取到HTML和所有的CSS了,那么接下来要做的便是将新的HTML替换老的HTML,并将所有通过link标签引入的css资源移除,换成style标签包裹的内联CSS

再看一下gererate函数,内部首先是对参数进行格式化处理

代码语言:javascript
复制
const generate = async (config: GenerateConfigParams) => {
  const generate = async (config: GenerateConfigParams) => {
    const {
      sourceDir, //源文件目录
      staticDir = sourceDir, // 静态资源打包目录
      pages, // 页面路由
      baseUrl = 'https://m.dewu.com',
      cdn = '',
      userAgent = DWUA,
      postProcess, // 处理生成后的html
      inject,
    } = config
    const mutation = typeof config.mutation === 'string' ? [config.mutation] : config.mutation
    const renderer = new Renderer()
    renderer.pages = pages
    renderer.baseUrl = baseUrl
    renderer.userAgent = userAgent
    const sourceHTMLPath = join(sourceDir, 'index.html')
    
    renderer.postProcess = ({ html, page }) => {
      if (mutation) {
        const dom = new JSDOM(sourceHTML)
        const newDom = new JSDOM(html)
        // 将新的HTML替换老的HTML
        mutation.forEach((mutation) => {
          const oldNode = dom.window.document.querySelector(mutation) as HTMLElement
          const newNode = newDom.window.document.querySelector(mutation) as HTMLElement
          if (oldNode) {
            oldNode.innerHTML = newNode?.innerHTML ?? ''
          }
        })
        
        // 将所有通过link标签引入的css资源移除,换成style标签包裹的内联CSS
        const links = newDom.window.document.querySelectorAll('link')
        for (const link of links) {
          const originHref = link.getAttribute('href') ?? ''
          const content = cssContent[originHref.split('/').pop() ?? '']
          if (content) {
            const newStyle = dom.window.document.createElement('style')
            newStyle.innerHTML = content
            newStyle.setAttribute('data-href', originHref)
            newStyle.setAttribute('data-css', 'inline')
            dom.window.document.head.appendChild(newStyle)
            const link = dom.window.document.querySelector(`link[href="${originHref}"]`)
            if (link) {
              link.parentElement?.removeChild(link)
            }
          }
        }

        html = dom.serialize()
      }
      
      return {
        html,
        page,
      }
    }

    renderer.onRequest = () => {
    // 上面的 onRequest 函数 ....
    }
  }
}

预渲染核心的三部分便大致如上述代码,

需要注意的是:接入预渲染的时候,需要找运维同学配合修改一些Nginx的配置,

主要是对路由 进行 文件重定向

慎用三方库

业务中存在一些简单的校验、转换和动效并不需要引入三方库,尤其是因为一个较为简单的功能引入了一个较为大且冷门的库时,不仅会增加项目的打包体积,还会增加项目后续维护的沟通、学习成本。

例如下面一个简单切换动效

img

是一个比较常规的切换动效,却在项目中引入了一个62.6kb大小的第三方库

img

该库的使用也是有一些学习成本,因为其具备实现比较复杂的动效能力,在业务动效具备一定复杂度且非首屏的场景下,是可以考虑引入使用的,否则类似这种首屏便需要加载的动效,还是慎重

上述的切换动效CSS实现代码如下

代码语言:javascript
复制
@keyframes bigScale {
  0% {
    opacity: 0;
    transform: scale(0.95);
  }

  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes smallScale {
  0% {
    transform: scale(1);
    opacity: 1;
  }

  to {
    transform: scale(0.95);
    opacity: 0;
  }
}

.squareInCenter {
  animation: 0.3s linear 0s 1 normal forwards running bigScale;
}

.squareOutCenter {
  animation: 0.3s linear 0s 1 normal forwards running smallScale;
}

在业务开发的过程中,尤其是C端的页面,在实现功能时对于引入额外的库是一件需要十分谨慎的事情,在内部就看到不少项目在引入关于日期处理方面的库时,dayjs、momentJS同时都会引用到项目中,B端项目都不能忍,更何况C端项目

字体使用和优化

字体加载和优化是前端开发中的一个重要问题,特别是在移动端和低网络状况下。下面是一些字体加载和优化的技巧

FOUT问题

通过设置 font-display 属性可以控制字体加载时的显示效果,包括 auto、swap、block、fallback 和 optional 几种模式,可以减少字体加载时间和防止文本闪烁

设置属性为fallback时效果

img

可以看到日期存在明显的FOUT(无样式文本闪现)问题,设置swap也是类似效果,并不符合预期

设置属性为block时效果

img

可以看到第一时间并没有渲染日期,而是有点的短暂空白,因为其可以避免 FOUT,字体文件必须在后台下载完全后,文本才能显示

最终选择了font-display: block;效果会更好一些

注意,并不是整个页面都使用block属性,对于一些非首屏关键渲染的样式,使用fallback更为合适一些,因为其会使用浏览器默认字体,所以还是需要结合业务、场景合理使用

字体****库大小,你得懂

先看一个gpt对于签到业务常用字体库打下的统计

DIN Condensed 字体库的大小在几百KB到几MB之间 Helvetica Neue 字体库的大小在几MB到十几MB之间

也就是这两种字体的大小,如果不加以处理,全部加载的大小在几MB到十几MB之间,对于前端项目而言,这是挺夸张的一件事

可以和设计人员沟通,将字体库中常用的字体导出,前端项目仅仅引入需要的字体就好,比如DIN Condensed字体都是使用在阿拉伯数字上,并不会在其他字上使用,那么只需要将阿拉伯数字导出即可。比如汉字,根据《现代汉语通用字表》(GB/T 13000-2018),常用汉字(包括简体字和繁体字)共计3500个,其中常用的一般是指前1000个左右的汉字,那么在使用字体库的时候,是不是可以默认只需要导出部分即可。

经过处理后的字体库大小如下图

img

字体****库数量,你得控制

上面说了一个字体库的大小是多大,就算是经过处理,最少也会有30KB大小,所以项目引入的字体种类是需要控制的,不能设计同学使用了多少种类字体设计,我们就要照单全收

当设计同学新增字体库时,如果字体使用在3次以内,是不是可以使用图片来代替文字,或者使用现有的字体库来平替

提高稳定性

在优化的过程中,移除了大量的废弃接口、ab和代码逻辑,这样做的代价必然是会造成一些问题,毕竟不管代码现状是怎样,只要线上能跑起来就是可以的,一般也不会大刀阔斧的去改造原有的代码,本着代码可维护性(避免日后接手代码的人内心🐶)的原则,最终还是对其动了手

既然已经选择了动手,那么就要思考一下如何确保稳定性,毕竟生产还是需要敬畏的,否则造成什么比较阻塞性的bug,那可真的是好心办坏事了,关于如何保证稳定性,我是从下面这几方面入手

可行性和风险评估

每次改动和优化代码之前,我会先对功能进行整体的回归一下,再查看对应的代码,在查看代码的过程中,我会确定几个事情

  1. 页面的功能是否存在多种业务逻辑判断的情况,比如符合条件A,执行弹框的逻辑,符合添加B,执行接口调用逻辑。若是业务逻辑较为复杂,那么改造和优化的成本会很大,ROI会比较低
  2. 是否为阻塞性的功能,比如新人引导流程的功能,这个流程出现问题则是非常严重的,这类代码保持能不动则不动
  3. 该功能模块迭代的频率,比如像商品流这块功能,仅近半年就经历过3次大的功能改版,每一次改版都是基于老的业务进行修修补补,导致代码就很难维护且这个痛经常存在,这种改造一下的ROI就会高一些
  4. 功能模块的大小和耦合度,比如下文说的MallScrollShowMore组件,其就是一个单独且较小的功能模块,改动速度快,且改造过程中关注点更多的只需要放在组件内部就好,这种改动ROI也会较高

结合上面的几点,我会综合考虑我接下来要做的事情可行性是怎样的,这么做带来的风险有多大,个人是的风格是,例如MallScrollShowMore组件,其就是一个非常独立的、功能小且非阻塞性功能的组件,可行性高,并不具备太多的改造风险,那么我就会撸起袖子干

做好记录和改动点

由于是优化中携带的代码改动,需要自己做好改动点的沉淀,不仅方便测试回归对应的地方,也便于自测

当发现组件设计或者实现有问题时,我会作如下的记录

严格执行自测

记录只是第一步,测试周开始前,需要针对本次的所有改动进行自测,因为很多改动在这个时候,作为研发的我才是最熟悉整个项目的了,这个时候其实个人才是测试主力,测试同学帮忙回归和验证核心流程。在改造过程中记录的改动点就是测试用例,需要严格执行和回归,毕竟功能优化、改造时,自己才是第一责任人

往期推荐

仿义务购App(Vue3+Pinia+Koa+Three.js 全栈项目)

wangEditor开源库作者:教你如何写出眼前一亮 简历

前端跨平台框架对比分析,看这篇就够了

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端Q 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 核心流程
  • 慎用三方库
  • 字体使用和优化
  • 提高稳定性
    • 可行性和风险评估
      • 做好记录和改动点
        • 严格执行自测
        相关产品与服务
        云服务器
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档