Continue to load HTML when – FThompson Jan 15 '19 at 20:07

  • @Vulcan Yes. synchronously. As if you referenced jQuery from the fastest CDN ever. – Royi Namir Jan 15 '19 at 20:08
  • @RoyiNamir So your example code would work as desired if you change the `src` instead of removing the element? – FThompson Jan 15 '19 at 20:10
  • @Vulcan Like I've said , i've tried to remove the element and also reference a new empty js SRC. nothing worked. once it start downloading the delayed file , it's unstoppable – Royi Namir Jan 15 '19 at 20:11
  • Another idea could be to set the script to `async` after the timeout period passes to allow the page to continue to load. – FThompson Jan 15 '19 at 20:12
  • @Vulcan that would make it run async and they said explicitly : _Note that we should not change the script to run asynchronously_ – Royi Namir Jan 15 '19 at 20:12
  • @RoyiNamir are we waiting for the script to load synchronously, or execute a block of synchronous code that takes a long time? – jmcgriz Jan 15 '19 at 20:57
  • @jmcgriz script to load synchronousl – Royi Namir Jan 15 '19 at 20:58
  • 5 Answers5

    1

    Since it has been pointed to my attention that you said this interview happened a "long time ago", below solution is probably not what they expected at that time.
    I will admit I have no idea what they were expecting then, but with today's APIs, it is doable:

    You can setup a ServiceWorker which will handle all the requests of your page as would do a proxy (but hosted in the browser), and which would be able to abort the request if it takes too long.
    But to do so, we need the AbortController API which is still considered an experimental technology, so once again, that may not be what they expected as an answer during this interview...

    Anyway, here is what our ServiceWorker could look like to accomplish the requested task:

    self.addEventListener('fetch', async function(event) {
      const controller = new AbortController();
      const signal = controller.signal;
    
      const fetchPromise = fetch(event.request.url, {signal})
        .catch(err => new Response('console.log("timedout")')); // in case you want some default content
    
      // 5 seconds timeout:
      const timeoutId = setTimeout(() => controller.abort(), 5000);
      event.respondWith(fetchPromise);
    });
    

    And here it is as a plnkr. (There is a kind of a cheat in that it uses two pages to avoid waiting once the full 50s, one for registering the ServiceWorker, and the other one where the slow-network occurs. But since the requirements say that the slow network happens "On occasions", I think it's still valid to assume we were able to register it at least once.)
    But if that's really what they wanted, then you'd even be better just to cache this file.

    And as said in comments, if you ever face this issue IRL, then by all means try to fix the root cause instead of this hack.

    Kaiido
    • 123,334
    • 13
    • 219
    • 285
    • Doesn't using `fetch` count as asynchronously loading the script? OP clearly states that the script is supposed to be synchronously loaded. – Neil VanLandingham Jan 19 '19 at 00:39
    • @Weft no, here the fetch is made from the Service Worker, but the html page still loads synchronously. Just try the plunker, you will see that you will wait 5 seconds before the document be ready but not the 50s. – Kaiido Jan 19 '19 at 01:27
    • Question says script tag. How does webworker can help here? – Royi Namir Mar 29 '19 at 18:44
    • @RoyiNamir who talked about WebWorkers? I talked about ServiceWorker. These are completely different and the latter does allow us to control how the resources on a page are fetched => it has everything to do with the question and even provides the only way to achieve the request. – Kaiido Mar 30 '19 at 00:24
    1

    My guess is that this was a problem they were trying to solve but had not, so they were asking candidates for a solution, and they would be impressed with anyone who had one. My hope would be that it was a trick question and they knew it and wanted to see if you did.

    Given their definitions, the task is impossible using what was widely supported in 2017. It is sort of possible in 2019 using ServiceWorkers.

    As you know, in the browser, the window runs in a single thread that runs an event loop. Everything that is asynchronous is some kind of deferred task object that is put on a queue for later execution1. Communications between the window's threads and workers' threads are asynchronous, Promises are asynchronous, and generally XHR's are done asynchronously.

    If you want to call a synchronous script, you need to make a call that blocks the event loop. However, JavaScript does not have interrupts or a pre-emptive scheduler, so while the event loop is blocked, there is nothing else that can run to cause it to abort. (That is to say, even if you could spin up a worker thread that the OS runs in parallel with the main thread, there is nothing the worker thread can do to cause the main thread to abort the read of the script.) The only hope you could have of having a timeout on the fetch of the script is if there were a way to set an operating-system-level timeout on the TCP request, and there is not. The only way to get a script other than via the HTML script tag, which has no way to specify a timeout, is XHR (short for XMLHttpRequest) or fetch where it is supported. Fetch only has an asynchronous interface, so it is no help. While it is possible to make a synchronous XHR request, according to MDN (the Mozilla Developer's Network), many browsers have deprecated synchronous XHR support on the main thread entirely. Even worse, XHR.timeout() "shouldn't be used for synchronous XMLHttpRequests requests used in a document environment or it will throw an InvalidAccessError exception."

    So if you block the main thread to wait for the synchronous loading of the JavaScript, you have no way to abort the loading when you have decided it is taking too long. If you do not block the main thread, then the script will not execute until after the page has loaded.

    Q.E.D

    Partial Solution with Service Workers

    @Kaiido argues this can be solved with ServiceWorkers. While I agree that ServiceWorkers were designed to solve problems like this, I disagree that they answer this question for a few reasons. Before I get into them, let me say that I think Kaiido's solution is fine in the much more general case of having a Single Page App that is completely hosted on HTTPS implement some kind of timeout for resources to prevent the whole app from locking up, and my criticisms of that solution are more about it being a reasonable answer to the interview question than any failing of the solution overall.

    • OP said the question came from "a long time ago" and service worker support in production releases of Edge and Safari is less than a year old. ServiceWorkers are still not considered "standard" and AbortController is still today not fully supported.
    • In order for this to work, Service Workers have to be installed and configured with a timeout prior to the page in question being loaded. Kaiido's solution loads a page to load the service workers and then redirects to the page with the slow JavaScript source. Although you can use clients.claim() to start the service workers on the page where they were loaded, they still will not start until after the page is loaded. (On the other hand, they only have to be loaded once and they can persist across browser shutdowns, so in practice it is not entirely unreasonable to assume that the service workers have been installed.)
    • Kaiido's implementation imposes the same timeout for all resources fetched. In order to apply only to the script URL in question, the service worker would need to be preloaded with the script URL before the page itself was fetched. Without some kind of whitelist or blacklist of URLs, the timeout would apply to loading the target page itself as well as loading the script source and every other asset on the page, synchronous or not. While of course it is reasonable in this Q&A environment to submit an example that is not production ready, the fact that to limit this to the one URL in question means that the URL needs to be separately hard coded into the service worker makes me uncomfortable with this as a solution.
    • ServiceWorkers only work on HTTPS content. I was wrong. While ServiceWorkers themselves have to be loaded via HTTPS, they are not limited to proxying HTTPS content.

    That said, my thanks to Kaiido for providing a nice example of what a ServiceWorker can do.

    1There is an excellent article from Jake Archibald that explains how asynchronous tasks are queued and executed by the window's event loop.

    Old Pro
    • 24,624
    • 7
    • 58
    • 106
    • [It is possible](https://stackoverflow.com/a/54212600/3702797) and they may very well wanted to check if the interviewee was aware of latests techs. Note that knowing about ServiceWorkers is a must in the industry nowadays, about AbortController, a bit less. – Kaiido Jan 20 '19 at 00:50
    • @Kaiido I updated my answer with an explanation of why I think ServiceWorkers do not fully answer the question, but I agree with you that might have been what the interviewer was going for. SMH – Old Pro Jan 20 '19 at 02:29
    • For point 1, I admit I completely missed the timing info, and you are probably right, ServiceWorker's solution might not be what they wanted at that time (though we don't really know how long ago it was). For the second point, you need to register the SW only once for it to work at every visit, so since the requirements states that this script is too slow "On occasions", I think we can assume that a solution for a second visit is acceptable, but I accept the criticism of this aspect, that I even formulate myself in my answer. For the third point though, as you seem to have understood yourself, – Kaiido Jan 20 '19 at 12:52
    • ...this is really just an MCVE, and of course, you can (and even should) be more granular when setting up such a system. You can be as granular as reading the content of the POST data of a request if you need to, so blacklisting a GET request is also doable. For the fourth one, no. ServiceWorkers work on all network requests (unfortunately not on blob:// ones...), even though the hosting page indeed needs to be served by https. But anyway, since LetsEncrypt, running your page from https is not what I call an "unacceptable constraint" anymore. – Kaiido Jan 20 '19 at 12:52
    • But overall, in the light of this new information, I can only agree with your answer ;-) – Kaiido Jan 20 '19 at 13:01
    0

    If the page doesn't reach the block that sets window.isDone = true, the page will be reloaded with a querystring telling it not to write the slow script tag to the page in the first place.

    I'm using setTimeout(..., 0) to write the script tag to the page outside of the current script block, this does not make the loading asynchronous.

    Note that the HTML is properly blocked by the slow script, then after 3 seconds the page reloads with the HTML appearing immediately.

    This may not work inside of jsBin, but if you test it locally in a standalone HTML page, it will work.

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width">
      <title>JS Bin</title>
      
      
    </head>
    <body>
      <script>
            
        setTimeout( function (){
           
         
          //    window.stop();
        
          window.location.href = window.location.href = "?prevent"
             
     
        }, 3000); //change here
      </script>
      
      <script>
        function getSearchParams(){
          return window.location.search.slice(1).split('&').reduce((obj, t) => {
            const pair = t.split('=')
            
            obj[pair[0]] = pair[1]
            
            return obj
          }, {})
        }
    
        console.log(getSearchParams())
        
        if(!getSearchParams().hasOwnProperty('prevent')) setTimeout(e => document.write('<script id ="s"  src="https://www.mocky.io/v2/5c3493592e00007200378f58?mocky-delay=4000ms"><\/script>'), 0)
      </script>
        <span>!!TEXT!!</span>
    </body>
    </html>
    jmcgriz
    • 2,819
    • 1
    • 9
    • 11
    • There is no other page than the one the script is within. – Royi Namir Jan 15 '19 at 20:43
    • It would still be one page, just with a flag set serverside. This doesn't work anyway, I just tested it and the timeout never fires when there's a really long synchronous action happening. – jmcgriz Jan 15 '19 at 20:45
    • @RoyiNamir I've updated this answer to use just the one page and be purely front end javascript in nature – jmcgriz Jan 15 '19 at 21:16
    • I already tried that ( wrote it also in the queation) . They say :(except this synchronous script). Other scripts are not to be touched – Royi Namir Jan 15 '19 at 21:18
    • @RoyiNamir Alright, this is my last shot at it :) It's the same approach, but in reverse - the default is to write the script tag to the page, but if the `prevent` flag is present, it won't be written and the script won't be requested. – jmcgriz Jan 15 '19 at 21:29
    -1

    Here is a solution the uses window.stop(), query params, and php.

    <html>
    <body>
    <?php if(!$_GET["loadType"] == "nomocky"):?>
    <script>
    var waitSeconds = 3; //the number of seconds you are willing to wait on the hanging script
    var timerInSeconds = 0;
    var timer = setInterval(()=>{
    timerInSeconds++;
    console.log(timerInSeconds);
      if(timerInSeconds >= waitSeconds){
        console.log("wait time exceeded. stop loading the script")
        window.stop();
        window.location.href = window.location.href + "?loadType=nomocky"
      }
    },1000)
    setTimeout(function(){
       document.getElementById("myscript").addEventListener("load", function(e){
         if(document.getElementById("myscript")){
            console.log("loaded after " + timerInSeconds + " seconds");
            clearInterval(timer);
         }
      })
    },0)
    </script>
    <?php endif; ?>
    
    <?php if(!$_GET["loadType"] == "nomocky"):?>
    <script id='myscript' src="https://www.mocky.io/v2/5c3493592e00007200378f58?mocky-delay=20000ms"></script>
    <?php endif; ?>
    
    <span>!!TEXT!!</span>
    </body>
    </html>
    
    Neil VanLandingham
    • 1,016
    • 8
    • 15
    -1

    You can use async property in script tag it will load your js file asynchronous. script src="file.js" async>

    Ved_Code_it
    • 704
    • 6
    • 10