预加载脚本但不执行

15

问题

为了提高页面性能,我需要预加载脚本,这些脚本将在页面底部运行

我想要控制脚本的解析、编译和执行时间。

我必须避免使用脚本标签,因为它会阻碍常见的渲染引擎(geeko等)。

我不能使用defer属性,因为我需要控制脚本何时执行。此外,async属性也不可行

示例:

<html><head>
//preload scripts ie: a.js  without use the script
</head><body> ..... all my nice html here
//execute here a.js
</body></html>

这让我能够最大化我的页面渲染性能,因为浏览器将开始下载脚本内容,并且同时并行地呈现页面。最后,我可以添加脚本标签,以便浏览器解析、编译和执行代码。
唯一的方法是使用一个隐藏的图像标签。(这是Stoyan的简化版本)
例如:
 <html><head>
 <img src="a.js" style=display:none;>
</head><body> ..... all my nice html here
 <script src="a.js">  
</body></html>

问题

我使用这个技术没有发现任何问题,但有人知道更好的方法吗? 是否有元预取?

附加信息

我正在使用 requirejs,因此我正在尝试预加载模块代码,而不执行它,因为这些代码依赖于DOM元素。


什么是“commons render engine”,为什么必须避免脚本标签?为什么不将要运行的脚本放入函数中,然后在DOM加载完成后使用jQuery执行该函数? - Sani Huttunen
动态追加的SCRIPT元素是非阻塞的... - Šime Vidas
@SaniHuttunen:我已经添加了信息,这些脚本是requirejs模块,并且我在底部加载了requirejs库。 - Martin Borthiry
@ŠimeVidas:这是真的,但你无法控制浏览器何时解析、编译和执行它们。 - Martin Borthiry
6个回答

6

使用类似的技术,您可以使用 img 在Internet Explorer中预加载脚本和样式表,在其他浏览器中则使用object标签。

var isMSIE = /*@cc_on!@*/false;

var resources = ['a.js', 'b.js', 'c.css'];

for (var i=0; i<resources.length; i++){
  if (isMSIE){
    new Image().src = resources[i];
  } else {
    var o = document.createElement('object');
    o.data = resources[i];
    document.body.appendChild(o);
  }
}

有一篇博客文章介绍了这种技术并概述了注意事项:预加载CSS/JavaScript而不执行

但是为什么不像其他答案建议的那样使用动态添加脚本呢?这可能会导致更干净、更具控制性的解决方案。


是的@Juicy,这似乎是解决问题的最佳方法。我的版本是那个脚本的简化副本。 为什么不使用动态添加的脚本?因为我想控制脚本何时被解析、编译和执行。你知道为什么在非IE浏览器上使用对象而不是图像更好吗? - Martin Borthiry
1
请注意,此建议已过时且不起作用。尝试在 URL 具有同源的 xframe 选项时加载带有对象的脚本标记,它会中断。将其移动到图像上,它就可以工作了。该帖子中列出的问题(它在 FF 中无法缓存)似乎不再适用。 - dalore
我尝试过这种方法,但是遇到了问题。我的Ember应用程序由大约150个单独的文件组成,由于页面上有这么多对象,浏览器变得非常缓慢。不幸的是,对我来说行不通。 - Varis Vītols
也许这会有所帮助? http://www.phpied.com/preload-cssjavascript-without-execution - user985399

5
您可以使用链接标签的prefetch属性来预加载任何资源,包括JavaScript。截至本文撰写(2016年8月10日),Safari不支持该功能,但几乎所有其他浏览器都支持:
<link rel="prefetch" href="(url)">
有关支持的更多信息,请参见此处:http://caniuse.com/#search=prefetch
请注意,因为Microsoft已终止对IE 9,10的支持,所以它们没有列在caniuse矩阵中。
有关更多信息,请参见此处以及此处提供了更多预加载选项,如预渲染等。

1
预取仅对下一页有用,对当前页面无效。因此,如果您的单页应用程序突然需要额外的JS模块,则该模块将不可用,并且仍需下载。 - Varis Vītols

1
对于每个想要下载但不执行的脚本,创建一个包含名称和url的对象,并将这些对象放入数组中。
循环遍历该数组,使用 jQuery.ajaxdataType: "text" 下载你的脚本作为文本。在 ajax 调用的 done 处理程序中,将文件的文本内容(传递的第一个参数)存储在相应的对象中,增加计数器,并在计数器等于以此方式下载的文件数时调用 "alldone" 函数。
在 "alldone" 函数中(或之后),执行以下操作:再次循环遍历数组,并为每个条目使用 document.createElement("script")document.createTextNode(...)(...scriptNode...).appendChild(...) 动态生成具有预期源内联的脚本,而不是通过 "src" 属性。最后,执行 document.head.appendChild(...scriptNode...),这是执行该脚本的时刻。
我在一个需要使用框架的项目中使用了这种技术,在那里几个框架和/或框架集需要相同的 JavaScript 文件,以确保每个文件仅从服务器请求一次。
代码(已测试并可用)如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
    <head>
        <script id="scriptData">
            var scriptData = [
                { name: "foo"    , url: "path/to/foo"    },
                { name: "bar"    , url: "path/to/bar"    }
            ];
        </script>
        <script id="scriptLoader">
            var LOADER = {
                loadedCount: 0,
                toBeLoadedCount: 0,
                load_jQuery: function (){
                    var jqNode = document.createElement("script");
                    jqNode.setAttribute("src", "/path/to/jquery");
                    jqNode.setAttribute("onload", "LOADER.loadScripts();");
                    jqNode.setAttribute("id", "jquery");
                    document.head.appendChild(jqNode);
                },
                loadScripts: function (){
                    var scriptDataLookup = this.scriptDataLookup = {};
                    var scriptNodes = this.scriptNodes = {};
                    var scriptNodesArr = this.scriptNodesArr = [];
                    for (var j=0; j<scriptData.length; j++){
                        var theEntry = scriptData[j];
                        scriptDataLookup[theEntry.name] = theEntry;
                    }
                    //console.log(JSON.stringify(scriptDataLookup, null, 4));
                    for (var i=0; i<scriptData.length; i++){
                        var entry = scriptData[i];
                        var name = entry.name;
                        var theURL = entry.url;
                        this.toBeLoadedCount++;
                        var node = document.createElement("script");
                        node.setAttribute("id", name);
                        scriptNodes[name] = node;
                        scriptNodesArr.push(node);
                        jQuery.ajax({
                            method   : "GET",
                            url      : theURL,
                            dataType : "text"
                        }).done(this.makeHandler(name, node)).fail(this.makeFailHandler(name, node));
                    }
                },
                makeFailHandler: function(name, node){
                    var THIS = this;
                    return function(xhr, errorName, errorMessage){
                        console.log(name, "FAIL");
                        console.log(xhr);
                        console.log(errorName);
                        console.log(errorMessage);
                        debugger;
                    }
                },
                makeHandler: function(name, node){
                    var THIS = this;
                    return function (fileContents, status, xhr){
                        THIS.loadedCount++;
                        //console.log("loaded", name, "content length", fileContents.length, "status", status);
                        //console.log("loaded:", THIS.loadedCount, "/", THIS.toBeLoadedCount);
                        THIS.scriptDataLookup[name].fileContents = fileContents;
                        if (THIS.loadedCount >= THIS.toBeLoadedCount){
                            THIS.allScriptsLoaded();
                        }
                    }
                },
                allScriptsLoaded: function(){
                    for (var i=0; i<this.scriptNodesArr.length; i++){
                        var scriptNode = this.scriptNodesArr[i];
                        var name = scriptNode.id;
                        var data = this.scriptDataLookup[name];
                        var fileContents = data.fileContents;
                        var textNode = document.createTextNode(fileContents);
                        scriptNode.appendChild(textNode);
                        document.head.appendChild(scriptNode); // execution is here
                        //console.log(scriptNode);
                    }
                    // call code to make the frames here
                }
            };
        </script>
    </head>
    <frameset rows="200pixels,*" onload="LOADER.load_jQuery();">
        <frame src="about:blank"></frame>
        <frame src="about:blank"></frame>
    </frameset>
</html>

其他问题 与上述方法密切相关 其他相关问题


0

为什么不试试这个呢?

var script = document.createElement('script');
script.src = 'http://path/to/your/script.js';
script.onload = function() {
  // do something here
}

document.head.appendChild(script);

你可以使用 .onload 事件来控制它何时加载。一个要注意的地方是 .onload() 在 IE 中不起作用,你可以使用以下代码:

script.onreadystatechange = function() {
  if (/^loaded|complete$/i.test(this.readyState)) {
    // loaded
  };
}

此外,通过DOM添加脚本是非阻塞的,我相信您可以完美地实现您的目标。

1
谢谢,我知道那个技巧,但我需要控制脚本何时被解析和执行。 - Martin Borthiry

0

我已经在那里回答了相同的问题:

https://dev59.com/kFHZs4cB2Jgan1zniN1h#46121439

只需使用<link>标签预加载您的脚本,然后您就可以使用<script>标签来调用它

例如:<link href="/js/script-to-preload.js" rel="preload" as="script">


0

不错的文章,我正在做类似于惰性求值的事情,但为了减少HTTP请求量,我将所有模块放在一个优化文件中。它类似于这个示例: Files = {}; Files['main.js'] = "require("controllers/app_controller.js");"; Files['controllers/app_controller.js'] = "alert("Hello world!");"; - Martin Borthiry
那么也许你应该考虑使用rake pipeline。这里有一个项目骨架,他们使用minispade和rake papeline来生成一个优化的文件:https://github.com/interline/ember-skeleton - txominpelu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接