【腾讯云前端性能优化大赛】两种非常规性能优化手段

方案一

代码语言:javascript
复制
const code = `
!(function () {
  "use strict"
  const assert = $$assert
  function createElement(tag) {
    const ele = document.createElement(tag.tagName)
    if (tag.attributes) {
      Object
        .keys(tag.attributes)
        .forEach(key => ele[key] = tag.attributes[key])
    }
    if (tag.innerHTML) {
      ele.innerHTML = tag.innerHTML
    }
    ele.async = false
    ele.defer = false
    return ele
  }
  function appendTag(tag) {
    document.body.appendChild(createElement(tag))
  }
  function inlineTag(tag, innerHTML) {
    let path
    if (tag.tagName.toUpperCase() === 'LINK') {
      tag.tagName = 'style'
      path = tag.attributes.href
      delete tag.attributes
    } else {
      path = tag.attributes.src
      delete tag.attributes.src
      tag.attributes.async = false
    }
    tag.innerHTML = \`// From CacheStorage: \${path} \\n\${innerHTML}\`
    return tag
  }
  const cacheName = 'assert'
  try {
    window.caches
      .open(cacheName)
      .then((cache) => {
        return assert.map((tag) => {
          if (tag.innerHTML) {
            return tag
          }
          if (!tag.attributes.src && !tag.attributes.href) {
            throw new Error(\`tag.innerHTML 与 tag.attributes.src、tag.attributes.href 必传一个, 当前为 \${JSON.stringify(tag)}\`)
          }
          const assertURL = tag.tagName.toUpperCase() === 'SCRIPT' ? tag.attributes.src : tag.attributes.href
          return cache.match(assertURL)
            .then((response) => {
              if (!response) {
                return cache
                  .add(assertURL)
                  .then(
                    () => cache
                      .match(assertURL)
                      .then(response => response.text())
                      .then(innerHTML => inlineTag(tag, innerHTML))
                  )
              } else {
                console.info(\`From CacheStorage: \${assertURL}\`)
                return response.text().then(innerHTML => inlineTag(tag, innerHTML))
              }
            })
        })
      })
      .then((scriptList) => {
        // Ensure the order of execution
        scriptList
          .reduce(
            (a, b) =>
              a
                .then(appendTag)
                .then(() => b)
          )
          .then(appendTag)
      })
  } catch (e) {
    console.error(e)
    setTimeout(() => {
      if (window.fundebug) {
        fundebug.notifyError(e)
      }
    }, 3000)
    const fragment = document.createDocumentFragment()
    assert.map(tag => {
      const ele = createElement(tag)
      fragment.appendChild(ele)
    })
    document.body.appendChild(fragment)
  }
})();
`

const compiler = source =>
require('@babel/core').transformSync(
source,
{
presets: [ '@babel/preset-env' ]
}
)
.code

class CacheStaticPlugin {
constructor(htmlWebpackPlugin, test) {
this.htmlWebpackPlugin = htmlWebpackPlugin
this.test = test
}
isTargetTag(tag) {
let link
const result = (tag.tagName === 'script' || tag.tagName === 'link')
&& !tag.innerHTML
&& tag.attributes
&& (link = (tag.attributes.src || tag.attributes.href))
&& (this.test instanceof RegExp ? this.test.test(link) : this.test(link))
if (!result) {
}
return !!result
}
injectCacheStaticCode(scriptList) {
return compiler(code.replace('$$assert', JSON.stringify(scriptList)))
}
apply(compiler) {
compiler.hooks.compilation.tap('StaticCachePlugin', compilation => {
const hooks = this.htmlWebpackPlugin.getHooks(compilation)
hooks.alterAssetTagGroups.tap('StaticCachePlugin', assets => {
assets.bodyTags = [
...assets.bodyTags.filter(tag => !this.isTargetTag(tag)),
{
tagName: 'script',
innerHTML: this.injectCacheStaticCode(assets.bodyTags.filter(this.isTargetTag.bind(this))),
closeTag: true
}
]
})
})
}
}

module.exports = CacheStaticPlugin

这是一个 Webpack 插件。使用时,通过传入特定的 正则表达式,筛选出需要缓存的静态 JS、CSS 文件,在 HTML 页面中注入一段代码。

当浏览器运行到这段代码时,带有特定标识符的 js、css 文件通过 cache.add() API 下载,并储存到 CacheStorage 中,接着把下载到的代码通过 script 标签注入到 HTML 中即可(注入时需考虑顺序问题)

优化思路

方案二

代码语言:javascript
复制
// cache
const OFFLINE_SUFFIX = '?offline';

let CURRENT_CACHES = {
offline: 'offline'
};

const cacheFiles = [
'/?offline'
];

self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
event.waitUntil(
caches.open(CURRENT_CACHES.offline).then(cache => {
return cache.addAll(cacheFiles);
})
);
});

const matchCache = async function(event, caches) {
const res = await Promise.all([
caches.match(event.request.url + OFFLINE_SUFFIX),
caches.match(event.request.url)
]);
if (res.some(e => e)) {
return res.filter(e => {
if (e) return e;
})[0];
} else {
return new Response();
}
};

const matchOfflineUrl = function(origin, event) {
if (event.request.method !== 'GET') {
return false;
}
return (
cacheFiles.some(url => origin + url === event.request.url) ||
cacheFiles.some(url => origin + url === event.request.url + OFFLINE_SUFFIX)
);
};

self.addEventListener('fetch', function(event) {
const origin = location.origin;
if (matchOfflineUrl(origin, event)) {
event.respondWith(
fetch(event.request.url)
.then(response => {
if (!response.ok) {
return caches.open(CURRENT_CACHES.offline).then(cache => {
event.request.url === origin + '/' && cache.add(cacheFiles[0]);
cache.put(event.request, response.clone());
return response;
});
} else {
return matchCache(event, caches).then(r => r);
}
})
.catch(err => {
console.error(err);
return matchCache(event, caches).then(r => r);
})
);
}
});

此方案为使用 Service Worker 技术中对 fetch 方法的监听,当 fetch 请求失败时,自动使用 CacheStorage 中的缓存进行返回

当用户再次进行联网时,更新缓存中储存的信息。

该方案与 Google Workbox 中某种策略方案类似

如果想了解其他前端性能优化方案,可以参考我另一篇文章:https://mp.weixin.qq.com/s/uly9sDgcUnuHdkfuEiUSXg

优化结果