Sticky

This blog has moved to www.dreamingincrm.com. Please update your feed Url. Thank you.

23 May 2015

Form Script Bundling and Minification

Ever since form script loading was made async, I have tried different ways to ensure that form scripts will be loaded in the sequence I want, and not how CRM wants it to be, a.k.a random. CRM2015 Update 1 has introduced Turbo Forms. Turbo Forms are supposed to drastically improve form render time. The problem of managing script dependencies, is still left to the developer. You basically have two options:
  1. In build stage, bundle and minify the scripts in the order of dependencies and use this in the form.
  2. Write self contained scripts, without any dependencies.
I tried out an approach using Plugin and couple of entities to try solve this. If you would rather read the code, you can download the source from https://github.com/rajyraman/DynamicScriptBundling. I have also included the managed and unmanaged solutions in the repo.

Components

 1. Form Load sequence entity, that stores the entity name and scripts that are to be loaded
 2. A blank javascript webresource, whose name is in this pattern: [entityname].crmform.min.js
 3. A plugin the runs on post-RetrieveMultiple on webresource entity

How it is wired up

[entityname].crmform.min.js is added to the form, that requires bunding and minification. This script just contains a comment, and nothing else.

Create a Form Load Sequence record, that specifies the entity name and the scripts that loaded be minified for this entity.


Register the plugin on post RetrieveMultiple of webresource entity.
How it works

If you have read the plugin code already, you already know. But you haven't here is how it works:
The plugin retrieves the Query key on the PluginExecutionContext's InputParameter that contains the QueryExpression object. It then checks, if there is a condition on this QueryExpression with name like crmform.min.js. If so, it retrieves the correct Form Load sequence record for the current entity.

Using the script sequence specified, it also retrieves the script webresources, concatenates and minifies them. This concatenated script is then used to update the OutputParameter's BusinessEntityCollection. The plugin also checks if the javascript webresources specified in the form load sequence entity, actually exists. It displays an error, if it doesn't.




ExecutionContext for RetrieveMultiple on webresource - Post Stage

<Profile>
  <Configuration i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ConstructorDurationInMilliseconds>2</ConstructorDurationInMilliseconds>
  <ConstructorException i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ConstructorStartTime>2015-05-22T14:04:15.5180427Z</ConstructorStartTime>
  <Context>
    <z:anyType i:type="PluginExecutionContext" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <BusinessUnitId>bf0d0d94-ddc9-e411-80db-c4346bad5414</BusinessUnitId>
      <CorrelationId>b668ab80-1dda-4676-8615-8d82e9ea0ff0</CorrelationId>
      <Depth>1</Depth>
      <InitiatingUserId>80151b28-fb9a-4d38-a920-d8f4d33ffcc2</InitiatingUserId>
      <InputParameters xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
        <a:KeyValuePairOfstringanyType>
          <b:key>Query</b:key>
          <b:value i:type="a:QueryExpression">
            <a:ColumnSet>
              <a:AllColumns>false</a:AllColumns>
              <a:Columns xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                <c:string>webresourceid</c:string>
                <c:string>name</c:string>
                <c:string>content</c:string>
                <c:string>webresourcetype</c:string>
                <c:string>silverlightversion</c:string>
              </a:Columns>
            </a:ColumnSet>
            <a:Criteria>
              <a:Conditions>
                <a:ConditionExpression>
                  <a:AttributeName>name</a:AttributeName>
                  <a:Operator>Equal</a:Operator>
                  <a:Values xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                    <c:anyType i:type="d:string" xmlns:d="http://www.w3.org/2001/XMLSchema">ryr_formexperiment.crmform.min.js</c:anyType>
                  </a:Values>
                  <a:EntityName i:nil="true" />
                </a:ConditionExpression>
              </a:Conditions>
              <a:FilterOperator>And</a:FilterOperator>
              <a:Filters />
            </a:Criteria>
            <a:Distinct>false</a:Distinct>
            <a:EntityName>webresource</a:EntityName>
            <a:LinkEntities />
            <a:Orders />
            <a:PageInfo>
              <a:Count>0</a:Count>
              <a:PageNumber>0</a:PageNumber>
              <a:PagingCookie i:nil="true" />
              <a:ReturnTotalRecordCount>false</a:ReturnTotalRecordCount>
            </a:PageInfo>
            <a:NoLock>false</a:NoLock>
          </b:value>
        </a:KeyValuePairOfstringanyType>
      </InputParameters>
      <IsExecutingOffline>false</IsExecutingOffline>
      <IsInTransaction>false</IsInTransaction>
      <IsOfflinePlayback>false</IsOfflinePlayback>
      <IsolationMode>2</IsolationMode>
      <MessageName>RetrieveMultiple</MessageName>
      <Mode>0</Mode>
      <OperationCreatedOn>2015-05-22T14:04:15.0754261Z</OperationCreatedOn>
      <OperationId>00000000-0000-0000-0000-000000000000</OperationId>
      <OrganizationId>32d64867-9081-49c2-8196-6304db7d47e1</OrganizationId>
      <OrganizationName>Contoso</OrganizationName>
      <OutputParameters xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
        <a:KeyValuePairOfstringanyType>
          <b:key>BusinessEntityCollection</b:key>
          <b:value i:type="a:EntityCollection">
            <a:Entities>
              <a:Entity>
                <a:Attributes>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>webresourceid</b:key>
                    <b:value i:type="z:guid">02fe0b47-8800-e511-80ef-c4346bada558</b:value>
                  </a:KeyValuePairOfstringanyType>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>name</b:key>
                    <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema">ryr_formexperiment.crmform.min.js</b:value>
                  </a:KeyValuePairOfstringanyType>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>content</b:key>
                    <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema">Ly9yeXJfZm9ybWV4cGVyaW1lbnQuY3JtZm9ybS5taW4uanM=</b:value>
                  </a:KeyValuePairOfstringanyType>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>webresourcetype</b:key>
                    <b:value i:type="a:OptionSetValue">
                      <a:Value>3</a:Value>
                    </b:value>
                  </a:KeyValuePairOfstringanyType>
                </a:Attributes>
                <a:EntityState i:nil="true" />
                <a:FormattedValues>
                  <a:KeyValuePairOfstringstring>
                    <b:key>webresourcetype</b:key>
                    <b:value>Script (JScript)</b:value>
                  </a:KeyValuePairOfstringstring>
                </a:FormattedValues>
                <a:Id>02fe0b47-8800-e511-80ef-c4346bada558</a:Id>
                <a:KeyAttributes xmlns:c="http://schemas.microsoft.com/xrm/7.1/Contracts" />
                <a:LogicalName>webresource</a:LogicalName>
                <a:RelatedEntities />
                <a:RowVersion>1308124</a:RowVersion>
              </a:Entity>
            </a:Entities>
            <a:EntityName>webresource</a:EntityName>
            <a:MinActiveRowVersion>-1</a:MinActiveRowVersion>
            <a:MoreRecords>false</a:MoreRecords>
            <a:PagingCookie><cookie page="1"><webresourceid last="{02FE0B47-8800-E511-80EF-C4346BADA558}" first="{02FE0B47-8800-E511-80EF-C4346BADA558}" /></cookie></a:PagingCookie>
            <a:TotalRecordCount>-1</a:TotalRecordCount>
            <a:TotalRecordCountLimitExceeded>false</a:TotalRecordCountLimitExceeded>
          </b:value>
        </a:KeyValuePairOfstringanyType>
      </OutputParameters>
      <OwningExtension xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
        <a:Id>34846b5c-8b00-e511-8101-c4346bade5b0</a:Id>
        <a:KeyAttributes xmlns:b="http://schemas.microsoft.com/xrm/7.1/Contracts" xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
        <a:LogicalName>sdkmessageprocessingstep</a:LogicalName>
        <a:Name>RYR.Experiments.PreRetrieveMultipleWebResource: RetrieveMultiple of webresource (Profiler)</a:Name>
        <a:RowVersion i:nil="true" />
      </OwningExtension>
      <PostEntityImages xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <PreEntityImages xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <PrimaryEntityId>00000000-0000-0000-0000-000000000000</PrimaryEntityId>
      <PrimaryEntityName>webresource</PrimaryEntityName>
      <RequestId i:nil="true" />
      <SecondaryEntityName>none</SecondaryEntityName>
      <SharedVariables xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <UserId>80151b28-fb9a-4d38-a920-d8f4d33ffcc2</UserId>
      <ParentContext i:nil="true" />
      <Stage>40</Stage>
    </z:anyType>
  </Context>
  <ExecutionDurationInMilliseconds>14</ExecutionDurationInMilliseconds>
  <ExecutionException i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ExecutionStartTime>2015-05-22T14:04:15.5180427Z</ExecutionStartTime>
  <HasServiceEndpointNotificationService>true</HasServiceEndpointNotificationService>
  <IsContextReplay>false</IsContextReplay>
  <IsolationMode>2</IsolationMode>
  <OperationType>Plugin</OperationType>
  <ProfileVersion>1.1</ProfileVersion>
  <ReplayEvents xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <SecureConfiguration i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <TypeName>RYR.Experiments.PreRetrieveMultipleWebResource</TypeName>
  <WorkflowInputParameters xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <WorkflowOutputParameters xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <WorkflowStepId i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
</Profile>

Performance
There is a bit of overhead, as the plugin has to retrieve and minify the webresources.


Scripts minified at build stage and added to the form

It takes 291ms.

Script dynamically bundled and minified by plugin

It takes 481ms.

Take this numbers, with a pinch of salt, as I found the performance can vary quite a bit depending on the time of the day, as I tested this in CRMOnline. I also tested this with cache disabled.

Failures along the way
I tried these approaches which, didn't quite workout, but I want to document them for future reference.
  1. Adding a non-existent form script into the Form Experiment's FormXml doesn't work, even though there is plugin to take care of the RetrieveMultiple request. CRM doesn't allow this to happen. The Javascript webresource has to exist, if you want to add this to a form. Here is what you'll have to add to the root node of the formxml, to hook up the webresource and associated onsave and onload event handlers for the form

    <formLibraries>
     <Library name='[JSWEBRESNAME]' libraryUniqueId='[NEWGUID]' />
    </formLibraries>
    <events>
     <event name='onload' application='false' active='false'>
      <Handlers>
       <Handler functionName='[FUNCNAME]' libraryName='[JSWEBRESNAME]' handlerUniqueId='[NEWGUID]' enabled='true' parameters='' passExecutionContext='false' />
      </Handlers>
     </event>
     <event name='onsave' application='false' active='false'>
      <Handlers>
       <Handler functionName='[FUNCNAME]' libraryName='[JSWEBRESNAME]' handlerUniqueId='[NEWGUID]' enabled='true' parameters='' passExecutionContext='false' />
      </Handlers>
     </event>
    </events>
    

  2. I tried to use YUICompressor.NET, but it is not strongly signed and had a dependency on Ecmascript.net, which is also not strongly signed. It seems too much pain to sign these third party assemblies and merge this with my plugin. If you love pain, have a look at http://buffered.io/posts/net-fu-signing-an-unsigned-assembly-without-delay-signing/ on how this can be done. I ended up using JSMin.net as it is just two files in total, and you can just add this to you project and build, instead of merging third-party assemblies.
My previous posts on the same subject
  1. http://nycrmdev.blogspot.com.au/2015/01/an-alternative-approach-to-loading-form.html
  2. http://nycrmdev.blogspot.com.au/2014/04/using-requirejs-in-crm2013.html
References

  1. Scott Durow - Ghost Webresources 
  2. Ben Hosk - Turbo Forms 
  3. MSDN - Write Code for CRM Forms 

No comments:

Post a Comment