Turbolinks: New head scripts not executing before turbolinks:load?

Created on 29 May 2016  路  16Comments  路  Source: turbolinks/turbolinks

If I visit a page that has a new script in the head, should there be any guarantee the script will start/finish loading/executing before turbolinks:load fires?

For example, let's say I have /some_page that includes new_script.js in its head:

# new_script.js
window.Something = 123
console.log("new_script.js done")

And I'm watching turbolinks:load somewhere else:

$(document).on 'turbolinks:load', ->
  console.log("in turbolinks:load")
  console.log(window.Something)

When I navigate to /some_page here's the console output:

in turbolinks:load
undefined
new_script.js done

Basically, the script doesn't load until after turbolinks:load fires, which is really counterintuitive. I'd expect the output to be:

new_script.js done
in turbolinks:load

Is this an issue or just asynchronous nature of how head scripts are appended?

Most helpful comment

@sstephenson Thanks for your reply, I lastly use the similar solutions.

All 16 comments

I recently met the same problem. Through reading the source code, it's maybe a "bug" about aysnc loading new head script.

In other word, the script of body will be executed usually than the script with src atrribute of head because of the poor network.

I didn't know it's a wrong usecase of me or it's a real bug about turbolinks. Need some discussions for me :)

Inserting a <script> element in the DOM implicitly makes it async:

// Existing element
document.querySelector("script").async // false
// Dynamic element
document.createElement("script").async // true

Because of this, Turbolinks Classic explicitly marks them async = false. Perhaps Turbolinks 5 should do the same.

@javan Thank you for your reply. You are absolutely right.

turbolinks5 use createElement to add new head script async, cause the problem above.

I think it should do the same as browser's default action.

What's your meaning about this ? :)

Turbolinks 5 does seem to copy the source script tag's existing attributes, which makes sense (code here). So if you specify <script type="text/javascript" src="..." async="false"> async should get copied by Turbolinks, I think.

However, when I do that I still encounter the same behavior described above. Tried it on both a remote script not on my server and on a script directly in my app's assets. Same results. I also tried adding createdScriptElement.async = false in turbolinks.js directly. No matter what I try it's not executing synchronously (or before turbolinks:load, at least). Using Chrome.

@windy If you add async="false" to your script tag, does that actually work for you?

@nerdcave Sorry to see your post later.

The conclusion is it's not work.

Firstly, add async=false to javascript_include_tag is not possible because Rails will drop this attribute if it sets to false. Then I manually add a script in the head like this:

<script src="<%= asset_path('test') async=false %>"></script>

But the output is not changed. It seems that Turbolinks just copy script attribute is not work for me.

Then I directly reference Turbolinks's newest coffee file. ( a lot of work for it ) And debuging for that. I aslo try a very hack method:

directly add

destinationElement.async=false to Turbolinks.Renderer#copyElementAttributes but aslo not work for me.

I have no idea for new try. It seems async load is not possible for some new browser( I use chrome Version 50.0.2661.102 (64-bit) in mac osx 10.11.5 )

Any advise for that? Thank you @nerdcave .

@javan @nerdcave Any process about this issue ?

@windy I wasn't able to find a solution that enforced synchronous loading, so I just disable Turbolinks for pages where this is a problem.

application.slim

head
...
  =javascript_include_tag :application, async: true, 'data-turbolinks-track': :true
...
  body data-page=(yield(:page).blank? ? nil : yield(:page))

home.slim

- content_for :page, 'pages/home'

page/home.coffee

initialize = ->
  # do page actions
  document.body.removeAttribute 'data-page'

document.addEventListener 'pages/home loaded', initialize
initialize() if document.body && document.body.getAttribute('loaded') == '' && document.body.getAttribute('data-page') == 'pages/home'

initializer.coffee

initialize = -> 
  document.body.setAttribute 'loaded', ''

  if page = document.body.getAttribute('data-page')
    e = document.createEvent('Event')
    e.initEvent "#{page} loaded", true, true
    document.dispatchEvent e

# Initialize if page loaded before application.js
if document.readyState == 'interactive'
  ready()

document.addEventListener 'turbolinks:load', initialize

application.js

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery2
//= require jquery_ujs
//= require turbolinks
//= require initializer

@Fudoshiki thank you for your reply, but it's not the answer of this question. we need this:

Page 1:

=javascript_include_tag :application, 'data-turbolinks-track': :true

Page2

=javascript_include_tag :application, 'data-turbolinks-track': :true
=javascript_include_tag :js1, 'data-turbolinks-track': :true

body
  javascript:
     // some js code after `js1` initialized

But not work now when we switch from page1 to page2 using turbolinks.

Updated solution

@Fudoshiki I don't think it's a good solution, I think it's a bug of turbolinks at this time. Waiting for a new reply from the contributors of it. Thanks anyway.

There鈥檚 no way to synchronously load a <script> in JavaScript. The best we can do is install an onload handler and wait for all dynamically added scripts to load before rendering the page, which is a significant amount of development work.

If you need this behavior, please feel free to explore it with a pull request. Otherwise, our official suggestion for now is: _don鈥檛 load scripts this way_. Either bundle all your dependencies together and load them on every page, or use one of the many JavaScript loaders (or implement your own) if you need to load additional dependencies at runtime.

@sstephenson Thanks for your reply, I lastly use the similar solutions.

Just for completeness sake... This was specifically an issue for me when loading Stripe.js remotely. Their docs state, "Stripe.js should be loaded directly from https://js.stripe.com/v2/," so bundling with your app isn't an option. The easiest solution for me was to just disable Turbolinks for pages that load that script.

@nerdcave Rather than disabling Turbolinks, you could also just make sure to include Stripe.js on the of every page.

@windy
How do you work?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Kiszko picture Kiszko  路  23Comments

AipackGit picture AipackGit  路  12Comments

sblackstone picture sblackstone  路  38Comments

coffeebite picture coffeebite  路  29Comments

jakehockey10 picture jakehockey10  路  31Comments