最近在折腾优化博客,由于全站都是部署在 Netlify、Vercel 的 CDN 上,都是海外节点,国内访问延迟高,在想办法优化下访问速度和体验。线路部分的优化可以看另一篇文章国外静态网站托管服务商国内速度对比及线路优化。这篇文章来探讨下前端 CSS 和 JS 资源的加载。

优化目标主要就两个:

  • 尽可能快,但是不希望我引入的辅助性第三方库影响到页面体验,不要阻塞主要内容渲染
  • 为了快第三方静态资源肯定是上 CDN,但是要有容灾,CDN 挂了要能 fallback 到其他 url

静态资源异步加载

prefetchpreload这些预加载技术暂且不谈,博客站点没那么多资源。这里主要是用asyncdefer来控制脚本异步加载,以避免阻塞 DOM 解析和页面渲染。

使用async异步加载,对于pangu这种会影响到内容呈现的使用async,对于统计类第三方库使用defer延迟加载,到 DOM 加载完再执行

有些第三方库需要手动 init,这样就不能使用deferasync关键字加载资源了,deferasync关键字只适用于远程资源,本以为把远程资源的<script>和内嵌在 HTML 里用来 init 的<script>都加上defer就可以利用它顺序加载的特性解决这个问题,结果发现不是这样的。。。。异步打乱脚本执行顺序,init 脚本不等待资源下载完毕会先执行。利用好onload事件可以解决这个问题。

before

<script src="https://cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js"></script>
<script>
    pangu.spacingElementByClassName('post');
    pangu.spacingElementByTagName('p');

    document.addEventListener('DOMContentLoaded', () => {
        pangu.autoSpacingPage();
    });
</script>

after

<script>
    function panguSpaing() {
        pangu.spacingElementByClassName('post');
        pangu.spacingElementByTagName('p');

        document.addEventListener('DOMContentLoaded', () => {
            // listen to any DOM change and automatically perform spacing via MutationObserver()
            pangu.autoSpacingPage();
        });
    }
</script>
<script async onload="panguSpaing()" src="https://cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js"></script>

除了onload还有onerror,还可以利用onerror事件在资源加载失败时 fallback 到其他 CDN,详见下文

这里跑了个测试看下优化前后的差距

优化前

image-20220506193639029

image-20220506193800266

First Contentful Pain Time 在 1.5s,整个页面到 1.5s 才能可见,DOM 加载总时间达到了 2.5s

本来我都已经把首页不需要加载的三方库都拿走了,但是还剩下一个,这个用于前端性能监控的 js 阻塞了渲染,挂的是腾讯云的 CDN,国内快到飞起,国外就卡到不行(作为对比上图里从 jsdelivr 加载的 js 也是快到飞起,墙内就慢得一批)

优化后

image-20220506193652188

优化后页面没有再阻塞,0.8s 页面就可用了,总 DOM 加载时间只有 1.5s,相较之前简直直接起飞。

cn

国内某检测平台的结果也显示优化效果明显

静态资源 Fallback 加载

国内好用的静态资源 CDN 还真没有,新浪那种只提供那么几个热门的库完全不符合需求,BootCDN 炸了不是一次两次,字节 CDN 的资源 URL 有够 ugly,URL 里面还带节点信息多半后面会出问题。360 奇舞 CDN 之前也换过域名、备案出问题,感觉每一个能省心用。七牛的目前看上去还行,也不知道后面会不会出什么幺蛾子。。。为了稳妥起见 + 照顾全球的访客,还是优先使用的 cloudflare 和 jsdelivr 的 CDN

网站主要三方库都是通过 jsdelivr 和 cloudflare 引入的,但是毕竟是海外的节点不知道哪一天就被墙了,所以还要为墙内的访客准备一个 fallback 方案。这里利用到script标签的onerror事件

现代浏览器都支持scriptlink标签上的onloadonerror事件,详细支持情况可以查看这里

https://pie.gd/test/script-link-events/

关于onerror,中文互联网上有好几篇文章说他会捕获script标签里的代码的执行错误。。。简直离谱。稍微查下 MDN 之类的文档就可以发现这玩意本来就是只捕获资源加载错误的

The onerror event is triggered if an error occurs while loading an external file (e.g. a document or an image).

https://www.w3schools.com/jsref/event_onerror.asp

利用好这个特性,就可以实现在资源加载失败时 fallback 从另一个 url 加载资源。

<script>
    function fallbackJSloader(url, loadedEvent) {
        console.log('Error loading asset, using fallback url');
        console.log('load asset from', url);

        let script = document.createElement("script");
        script.type = "text/javascript";
        script.src = url;
        script.async = true;
        if (loadedEvent)
            script.setAttribute('onload', `${loadedEvent}()`)
        document.body.appendChild(script);
    }
</script>

<script defer onload="init_gitalk()"
onerror="fallbackJSloader('https://cdn.staticfile.org/gitalk/1.7.2/gitalk.min.js','init_gitalk')"
src="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js">

CSS 的 fallback 加载同理,此处不再赘述

优化后如图,在开发者工具中手动 block 资源网站,可以自动 fallback 从另一个 url 加载资源,这下网站的可用性又提升了一点点🤏哈哈哈

66230832-3581-42DF-9F2F-B817E88DAF4D

扩展阅读

网站 profile工具