h5数据预取方案

背景

在页面的性能优化中,资源加载是决定页面展示时间的重中之重。为了减少资源的请求,有两个常见的解决方案:

  1. 一个是SSR服务端渲染页面,这样可以减少页面的初始化数据请求,页面在服务端接受到请求时进行数据请求与渲染页面,最终返回给客户端一个可以直接展示的html页面(直出)。但这个方案需要服务器的支持。
  2. 另一方是SSG服务端生成页面。与SSR的区别是,SSG在构建时就会生成html文件,后续的数据获取需要客户端加载了页面资源后再进行。SSG的优势是首屏加载速度快,可以直接分发资源到CDN,减少服务器压力。更进一步可以使用离线包方案,客户端使用本地资源打开页面,最大化减少网络的影响。

目前我们采用了第二种方案:SSG与离线包相结合。但在动态页面中,页面快速展示后,可以看到明显的页面卡顿,这是由于页面需要从服务端获取动态数据。本文便是为了解决这一问题。

期望效果

期望能结合目前的离线包方案在页面展示时,没有卡顿,能直接获取到动态数据并展示。

注意事项:

  1. 目前项目中存在离线包与在线包两种环境的页面,离线包方式是通过客户端代理页面的方式实现,两个包的域名不同,所以在获取动态数据时,需要注意不同包的数据获取。
  2. 部分页面的数据与当前时间状态深度绑定,这种情况下应避免得页面预取数据与页面展示相隔太久。

解决方案

  1. 例如百度(CloudHybrid)、腾讯(VasSonic)等大厂都在公司内部研发了相应的框架,特点是客户端在请求页面时,提前缓存页面与请求服务端获取动态数据,再根据获取到的数据与缓存页面进行水合。但该方案需要全端配合,复杂度较高。
  2. 最简单的方案就是在客户端初始化时,后台加载一个预取数据页面,这个页面会将数据存储起来,后续前端使用时直接调用。这个方案大幅前置了数据预取的过程,所以在页面首屏加载后,仍需要再次请求接口更新数据,防止期间数据变动,且发生数据变动时会导致页面数据闪动。

由于人力与成本考虑,项目使用第二种方案,下面介绍详细流程:

  1. 客户端在用户打开APP,初始化应用时,在后台加载预取数据页面。
  2. 预取数据页面从服务端获取动态数据。
  3. 将数据存储至本地缓存。这里建议使用localforage库,该库会自动选择IndexedDB、WebSQL、localStorage中的一种存储方式,且自动进行类型转换。
  4. 进入应用后,用户访问功能页面。
  5. 页面先使用本地缓存数据展示页面。这里注意由于页面使用ssg的方案,如果展示加载的骨架屏,依然会出现数据从无到有的闪动出现。所以需要直接将页面首屏置空,等js加载完成获取到本地数据后再显示页面(使用useEffect触发)。
  6. 由于动态数据可能更新,所以在页面加载后,需要再次请求服务端接口,更新页面(如数据变动,会造成页面闪动)。
  7. 最后将更新后的数据再次存入本地存储。
h5数据预取方案.png

参考

alloyteam - h5秒开方案大全

百度APP-Android H5首屏优化实践

附录

// 结合localforage的hooks
import { useCallback, useEffect, useRef, useState } from 'react';
import localforage from 'localforage';

let EventMap = new Map<String, Function[]>();
class EventEmitter {
  static on<T>(key: string, callback: (value: T) => void) {
    if (EventMap.has(key)) {
      EventMap.get(key)?.push(callback);
    } else {
      EventMap.set(key, [callback]);
    }
    return () => {
      const funcList = EventMap.get(key);
      EventMap.set(
        key,
        funcList!.filter((func) => func !== callback),
      );
    };
  }
  static emit<T>(key: string, value: T) {
    if (EventMap.has(key)) {
      EventMap.get(key)?.forEach((func) => {
        func(value);
      });
    }
  }
}

export function useLocalStorage<T>(
  key: string,
  defaultValue: T,
  pathname?: string,
) {
  const refKey = useRef(key);
  const refInit = useRef(false);

  function getStoredValue() {
    return new Promise<T>((resolve) => {
      localforage
        .getItem(refKey.current)
        .then((raw) => {
          if (typeof raw !== 'undefined' && raw !== null) {
            resolve(raw as T);
          } else {
            resolve(defaultValue);
          }
        })
        .catch((e) => {
          console.error(e);
          resolve(defaultValue);
        });
    });
  }

  const [state, setState] = useState<T>(defaultValue);
  const [initSetList, setInitSetList] = useState<Function[]>([]);

  useEffect(() => {
    const path = pathname || location.pathname.replace(/\//g, '_');
    refKey.current = `${path}-${key}`;
    getStoredValue().then((value) => {
      setState(value);
      if (initSetList.length) {
        initSetList.forEach((func) => func());
      }
    });
    refInit.current = true;
  }, [key]);

  useEffect(() => {
    const handleEventEmitter = (eventValue: T) => {
      if (JSON.stringify(eventValue) !== JSON.stringify(state)) {
        updateState(eventValue, true);
      }
    };
    const removeHandler = EventEmitter.on<T>(key, handleEventEmitter);
    return () => {
      removeHandler();
    };
  }, [state]);

  const updateState = useCallback(
    (value: T, isEmit?: boolean) => {
      function updateForageState(currentState: T) {
        setState(currentState);

        if (typeof currentState === 'undefined') {
          localforage.removeItem(refKey.current);
        } else {
          localforage
            .setItem(refKey.current, currentState)
            .then(() => {
              if (!isEmit) {
                console.log('emit');
                EventEmitter.emit(key, currentState);
              }
            })
            .catch((e) => {
              console.error(e);
            });
        }
      }
      if (!refInit) {
        setInitSetList((list) => [
          ...list,
          updateForageState.bind(useLocalStorage, value),
        ]);
      } else {
        updateForageState(value);
      }
    },
    [refKey, refInit],
  );

  return [state, updateState] as const;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 数据实时:即数据库中的数据得到更新,页面立刻就想得到更新并展示最新的数据状态。通常使用在大数据可视化分析,运营数据...
    果汁凉茶丶阅读 6,237评论 1 5
  • H5性能优化意义 对于一个H5的产品,功能无疑很重要,但是性能同样是用户体验中不可或缺的一环。原本H5的渲染性能就...
    7ece657ee3b6阅读 1,601评论 0 2
  • H5性能优化意义 对于一个H5的产品,功能无疑很重要,但是性能同样是用户体验中不可或缺的一环。原本H5的渲染性能就...
    望月成三人阅读 994评论 1 10
  • H5性能优化方案 H5性能优化意义 对于一个H5的产品,功能无疑很重要,但是性能同样是用户体验中不可或缺的一环。原...
    自然心情阅读 8,070评论 0 14
  • 前言 现在许多app都嵌入了H5页面, 然而WebView加载速度慢这个问题却一直影响着用户的体验, 所以本文就如...
    迷途小码农h阅读 9,209评论 0 18