在webpack上,如何在不评估脚本的情况下导入脚本?


9

我最近正在从事一些网站优化工作,并且通过使用如下的import语句开始在webpack中使用代码拆分:

import(/* webpackChunkName: 'pageB-chunk' */ './pageB')

哪个可以正确创建pageB-chunk.js,现在让我们说我想在pageA中预取此块,我可以通过在pageA中添加以下语句来做到这一点:

import(/* webpackChunkName: 'pageB-chunk' */ /* webpackPrefetch: true */ './pageB')

这将导致

<link rel="prefetch" href="pageB-chunk.js">

附加到HTML的头部后,浏览器会预取它,到目前为止效果很好。

问题是我在这里使用的import语句不仅预取js文件,还评估js文件,这意味着该js文件的代码已解析并编译为字节码,并执行了该JS的顶级代码。

这是在移动设备上非常耗时的操作,我想对其进行优化,我只想要预取部分,而不想要评估和执行部分,因为稍后当某些用户交互发生时,我将触发解析并自我评估

在此处输入图片说明

↑↑↑↑↑↑↑↑我只想触发前两步,图片来自https://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/ ↑↑↑↑↑↑↑ ↑↑

当然,我可以通过自己添加预取链接来做到这一点,但这意味着我需要知道应该在预取链接中放入哪个URL,webpack绝对知道该URL,如何从webpack中获取它?

webpack有没有简单的方法来实现这一目标?


if (false) import(…)-我怀疑webpack会进行死代码分析吗?
Bergi

在哪里/当你真的想评估模块?这就是动态import代码应该去的地方。
Bergi

我现在很困惑。为什么评估很重要?因为最后,应该由客户端浏览器设备评估JS文件。否则我无法正确理解问题。
AmerllicA

@AmerllicA最终是应该对js进行评估,但是认为这种情况:我的网站有A,B两页,A页的访问者在完成“ A页”的某些工作后经常访问B页。那么合理地预取B页的JS,但是如果我可以控制评估B的JS的时间,那么我可以100%确保在访问者尝试“完成其工作”在A页上时不会阻塞产生毛刺的主线程。我可以评估访客单击指向页面B的链接后,B的JS,但是那时B的JS最有可能被下载,我只需要花一点时间对其进行评估。
migcoder

根据chrome v8的博客(v8.dev/blog/cost-of-javascript-2019)的肯定,他们通过利用Worker线程和许多其他技术进行了大量优化,以实现极快的JS解析时间,请参见youtube.com。 / watch?v = D1UJgiG4_NI。但是其他浏览器尚未实现这种优化。
migcoder

Answers:


2

更新

您可以将preload-webpack-pluginhtml-webpack-plugin一起使用,它将定义要在配置中预加载的内容,并会自动插入标签以预加载您的块

请注意,如果您现在使用的是webpack v4,则必须使用 preload-webpack-plugin@next

plugins: [
  new HtmlWebpackPlugin(),
  new PreloadWebpackPlugin({
    rel: 'preload',
    include: 'asyncChunks'
  })
]

对于使用动态名称(例如chunk.31132ae6680e598f8879.js和) 生成两个异步脚本的项目chunk.d15e7fdfc91b34bb78c4.js,以下预加载将注入到文档中head

<link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">
<link rel="preload" as="script" href="chunk.d15e7fdfc91b34bb78c4.js">

更新2

如果您不想预加载所有异步块,而仅预加载一次,也可以

您可以使用migcoder的babel插件,也可以preload-webpack-plugin像下面这样使用

  1. 首先,您将不得不借助webpack magic comment示例来命名该异步块

    import(/* webpackChunkName: 'myAsyncPreloadChunk' */ './path/to/file')
  2. 然后在插件配置中使用该名称,例如

    plugins: [
      new HtmlWebpackPlugin(),   
      new PreloadWebpackPlugin({
        rel: 'preload',
        include: ['myAsyncPreloadChunk']
      }) 
    ]

首先,让我们看看指定script标签或link标签来加载脚本时浏览器的行为

  1. 每当浏览器遇到script标签时,它将加载该标签以解析并立即执行
  2. 你只能延缓解析,并帮助评估asyncdefer标签只有等到DOMContentLoaded事件
  3. 如果您不插入脚本标签(只能将其预加载),则可以延迟执行(评估link

现在还有其他一些不建议使用的 hackey方法是您发送整个脚本和string/ comment(或(因为注释或字符串的评估时间几乎可以忽略)),并且在需要执行时可以使用Function() constructoreval不建议同时使用


另一个方法 服务工作者:(这将在页面重新加载后保留您的缓存事件,或者在加载缓存后用户脱机)

在现代浏览器中,您可以service worker用来获取和缓存资源(JavaScript,图像,css any),当对该资源的主线程请求时,您可以拦截该请求并以这种方式从缓存中返回资源,而无需在以下情况下解析和评估脚本:您正在将其加载到缓存中,在此处了解有关服务人员的更多信息

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        '/sw-test/star-wars-logo.jpg',
        '/sw-test/gallery/bountyHunters.jpg',
        '/sw-test/gallery/myLittleVader.jpg',
        '/sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(caches.match(event.request).then(function(response) {
    // caches.match() always resolves
    // but in case of success response will have value
    if (response !== undefined) {
      return response;
    } else {
      return fetch(event.request).then(function (response) {
        // response may be used only once
        // we need to save clone to put one copy in cache
        // and serve second one
        let responseClone = response.clone();

        caches.open('v1').then(function (cache) {
          cache.put(event.request, responseClone);
        });
        return response;
      }).catch(function () {
        // any fallback code here
      });
    }
  }));
});

如您所见,这与webpack无关,这不在webpack的范围之内,但是在webpack的帮助下,您可以拆分软件包,这将有助于更好地利用Service Worker


但是,我的痛点仍然是我无法从webpack轻松获取文件的url,即使我使用了SW,我仍然需要让SW知道应该预缓存哪些文件... webpack清单插件可以将清单信息生成到SW,但它是一个全功能设备,意味着SW别无选择,只能预先缓存清单中列出的所有文件...
migcoder

理想情况下,我希望webpack可以添加另一个魔术注释,例如/ * webpackOnlyPrefetch:true * /,这样我可以为每个懒惰的可加载块调用两次import语句,一个用于预取,一个用于代码评估,并且一切都按需进行。
migcoder

1
@migcoder是一个有效的点(您无法获取文件名,因为它是在运行时动态生成的),如果我可以找到任何解决方案,那么它将寻找任何解决方案
Tripurari Shankar

@migcoder我已经更新了答案,请查看它可以解决您的问题
Tripurari Shankar

它解决了部分问题,可以过滤掉很好的异步块,但是我的最终目标是仅预取所需的异步块。我目前正在查看此插件github.com/sebastian-software/babel-plugin-smart-webpack-import,它向我展示了如何收集所有导入语句并基于魔术注释进行一些代码修改,也许我可以创建一个类似的插件,可在带有“ webpackOnlyPrefetch:true”魔术注释的导入语句上插入预取代码。
migcoder

1

更新: 我将所有内容都包含在npm包中,请检查一下! https://www.npmjs.com/package/webpack-prefetcher


经过几天的研究,我最终编写了一个定制的babel插件...

简而言之,插件的工作方式如下:

  • 收集代码中的所有import(args)语句
  • 如果import(args)包含/ *预取:true * /注释
  • import()语句中找到chunkId
  • 将其替换为Prefetcher.fetch(chunkId)

Prefetcher是一个帮助器类,其中包含webpack输出的清单,并且可以帮助我们插入预取链接:

export class Prefetcher {
  static manifest = {
    "pageA.js": "/pageA.hash.js",
    "app.js": "/app.hash.js",
    "index.html": "/index.html"
  }
  static function fetch(chunkId) {
    const link = document.createElement('link')
    link.rel = "prefetch"
    link.as = "script"
    link.href = Prefetcher.manifest[chunkId + '.js']
    document.head.appendChild(link)
  }
}

一个用法示例:

const pageAImporter = {
  prefetch: () => import(/* prefetch: true */ './pageA.js')
  load: () => import(/* webpackChunkName: 'pageA' */ './pageA.js')
}

a.onmousehover = () => pageAImporter.prefetch()

a.onclick = () => pageAImporter.load().then(...)

这个插件的细节可以在这里找到:

预取-从Webpack进行控制

再说一次,这是一种非常骇人听闻的方式,我不喜欢它,如果您希望Webpack团队实现这一点,请在这里投票:

功能:按需预取动态导入


0

假设我了解您要实现的目标,则想在给定事件(例如,单击按钮)之后解析并执行模块。您可以简单地将import语句放入该事件中:

element.addEventListener('click', async () => {
  const module = await import("...");
});
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.