Ever since the script Web Resource loading behaviour changed in UR12 from series to parallel I've been interested in how to best standardise management the loading of script Web Resources. The fact that scripts are loaded from a variety of places makes the subject even more interesting. This post attempts to answer all of my questions about how scripts are loaded.
In order to fully understand how CRM 2013 loads scripts I examined the following script load points:
1. Form Libraries
Each form can specify multiple libraries, and a set of Events Handlers that are called when the form is loaded.
2. Ribbon Xml Command Actions
Command Bar JavaScript functions that are called when a Command Bar button is clicked. Multiple JavaScriptFunctions can be included in a Command Action and is often used to specify dependency scripts. The following example shows common.js being a dependency to library.js. The 'isNaN' function is simply a dummy function call because common.js only needs to be loaded.
<Actions>
<JavaScriptFunction FunctionName="isNaN" Library="$webresource:new_common.js" />
<JavaScriptFunction FunctionName="fooBar" Library="$webresource:new_library.js" />
</Actions>
3. Ribbon Xml Enable Rule
Enable Rule JavaScript functions are evaluated to determine if a Command Bar is enabled (visible). Again, multiple functions can be specified for an enable Rule and is often used to specify dependency scripts in the same way as shown above for Command Actions.
<EnableRule Id="new.new_valuerulestest.BoolYes.EnableRule">
<CustomRule FunctionName="isNaN" Library="$webresource:new_ common.js" />
<CustomRule FunctionName="fooBar" Library="$webresource:new_ library.js" />
</EnableRule>
Approach
I created 6 scripts that each used console.log to write messages as they were loaded and when the functions were being executed. I also used Fiddler to introduce latency into the script downloads so that I could see the order that they were being requested. The terminology I use is in the results are as follows:
- Script Download - The process of downloading the script web resource from the server.
- Script Load – The actual evaluation of the script once it has been downloaded from the server. The script load defines any functions and object prototypes. If you have any code that is not contained within a function, then it will be executed at this time. Script load order becomes important when the Script Load includes references to functions that exist in other libraries such as name spacing functions or jQuery constructs. It is advisable to keep these kind of dependencies to a minimum due to requirement to have scripts loaded in a particular order.
- Script Event Handler – Function registrations that are called on form events such as OnLoad or OnChange that call a function in a loaded script web resource. The Event Handers are different to the Script Load in that the Event Handler is called after the Script has been loaded.
- Executed – If there is a function that is referenced by an Enable Rule or Command, the function is executed after the script is loaded.
Here are the things you need to know!
The following section summaries the results I discovered from my deep-dive:
Form Scripts
1) Form Script will download in parallel unless they are already loaded by an EnableRule.
2) Form Scripts are not guaranteed to load in the order that they are specified in the form properties. They will be requested asynchronously in parallel and then loaded in the order that they received from the server. This is similar to the CRM2011 UR12-UR14 behaviour meaning that you can't guarantee that a dependency script is loaded before a dependant script without using some custom code to handle dependencies.
3) Form Script OnLoad Event Handlers will execute only after ALL script specified in the Form properties are loaded.
4) Form Scripts that are loaded only by the Form (and not Enable Rules) will appear as a 'normal' script element with WebResource URL in the IE F12 debugger.
Enable Rules
1) EnableRule CustomRule Scripts are downloaded, loaded & executed in series and in the order they are specified in the EnableRule. The next script is loaded after the previous one has been loaded and executed.
2) EnableRule functions are executed immediately after their script is loaded and before the OnLoad form even handler. The function is re-evaluated if the ribbon is refreshed and when the button is clicked (before the command action is executed).
3) Form Scripts will start downloading after all Enable Rule Scripts have been loaded and executed. The Command bar is then available before the form on-load event has been run.
4) An interesting by-product of the fact that Enable Rules load in series before the form scripts is that if you specify the same scripts in an Enable Rule that are referenced by the Form, your scripts will always load in the correct order! I had some situations like this on an upgrade from CRM2011 and I couldn't work out why on some forms the scripts were loaded in series and some in parallel.
5) Scripts loaded by EnableRules will appear as dynamic 'script block' and not a named WebResource URL in the IE F12 debugger.
Commands
1) Command Action JavaScript Function scripts are loaded and executed in series and in the order that they are specified in the Command.
2) Command Action scripts are loaded when the button is clicked and not when the page is loaded (unless they are used by the form or enable rules in which they are already loaded).
3) Scripts loaded by Command Actions will appear as dynamic 'script block' and not a named URL in the IE F12 debugger.
4) I found an interesting boundary condition that if the same scripts were used in the form load and in command actions; you could click the button before all the scripts were loaded so that only part of the command would run. This would have the potential to cause script errors if a user clicked on a command button before the on-load event had fired.
General
1) Scripts are loaded only once per page even if they are referenced in multiple places (Commands, EnableRule and Form Libraries).
Script Loading Process Flow
So that you can fully understand the script loading process, I've attempted to show a good approximation in flow chart form.
Key points of note on the flow:
- Enable Rule Scripts are loaded in series
- Form Scripts are loaded in Parallel.
- Command Action Scripts are loaded in series
-
Command Actions can be executed before the Form Scripts are loaded.
Working around the parallel loading of Form Scripts
When parallel loading of scripts first became an issue in CRM2011 with UR12 I used a timeout style approach to waiting for dependant scripts to load. You can read more about this on my blog post on the subject.
Now rather than use a timeout wait, I feel that a more deterministic approach would be more suited. By wrapping the script web resources in the following, my tests showed that were always loaded in the correct order. The scriptLoader uses a dependency registration mechanism that allows registration of scripts to be loaded only when all of the dependant scripts are also loaded.
var scriptLoader = scriptLoader || {
delayedLoads: [],
load: function (name, requires, script) {
window._loadedScripts = window._loadedScripts || {};
// Check for loaded scripts, if not all loaded then register delayed Load
if (requires == null || requires.length == 0 || scriptLoader.areLoaded(requires)) {
scriptLoader.runScript(name, script);
}
else {
// Register an onload check
scriptLoader.delayedLoads.push({ name: name, requires: requires, script: script });
}
},
runScript: function (name, script) {
script.call(window);
window._loadedScripts[name] = true;
scriptLoader.onScriptLoaded(name);
},
onScriptLoaded: function (name) {
// Check for any registered delayed Loads
scriptLoader.delayedLoads.forEach(function (script) {
if (script.loaded == null && scriptLoader.areLoaded(script.requires)) {
script.loaded = true;
scriptLoader.runScript(script.name, script.script);
}
});
},
areLoaded: function (requires) {
var allLoaded = true;
for (var i = 0; i < requires.length; i++) {
allLoaded = allLoaded && (window._loadedScripts[requires[i]] != null);
if (!allLoaded)
break;
}
return allLoaded;
}
};
scriptLoader.load("Script Name", [("Dependency Script 1","Dependancy Script 2"}, function () {
// TODO: Your script goes here!
});
You must encapsulate every form script that is a dependant or a dependency. The load function has the following parameters:
- Name – Specify a unique name of each of your scripts
- Requires – Specify an array of script names that must be loaded before the current one is loaded.
Of course, the script can be minified to reduce its impact on your individual Web Resources.
Conclusions
Armed with the information I've discovered in this deep-dive, I feel I'm equipped to make the right decision when designing solutions that include multiple script Web Resources.
@ScottDurow