In March 2020, with support for compile-time solutions, the Rax applets released a version that supports run-time solutions. To date, Rax remains the only applet development framework in the industry that supports both compile-time and run-time solutions. This article will introduce the principle of Rax applet runtime scheme and our thinking.

Review the compile-time scheme

Before getting to the run-time scenario, let’s review what a compile-time scenario is. As the name implies, compile-time solutions focus on compilation, which is represented by the Taro V2.x framework. It converts JSX into applets’ template language (i.e. WXML/AXML, etc.) through static compilation. With lightweight runtime JS code, the applets’ life cycle and React life cycle are completely different, enabling users to develop applets with the familiar React DSL. The principle of compile time scheme of Rax is similar to Taro v2.x. For details of implementation, please refer to the previous article Rax to Applet link Principle analysis (1) and Rax applet compile time scheme principle analysis. Unlike the compile-time scheme, the run-time scheme focuses on implementing rendering capabilities at run time and does not rely on static compilation, so it has few syntactic restrictions, which is its biggest feature. Let’s look at how the run-time scenario implementation works.

The birth of foundation

The low-level implementation of applets is actually based on Web technology, but when it comes to developers, it is very different from the Web. In the applet, the logical layer is isolated from the view layer. The logical layer passes data to the view layer through a unique setData method to trigger rendering, while the view layer triggers the logical layer code through events. Its architecture is shown in the figure below. Compared to Web development, where developers can use JS to invoke browser-provided DOM/BOM APIS to render content at will, the architecture of applets is more closed and secure, but it also means that Web code cannot run directly on applets.

Modern front-end frameworks (React/Vue) use the DOM API to create views. The view-layer template of the applet is pre-written by the developer, which means that dynamic DOM creation is not allowed in the applet. However, the “self-reference” nature of custom components of applets opens the door to dynamic DOM creation. By self-reference, a custom component can use itself as a child node, which means that we can construct any level and number of DOM trees by recursive reference.

For example, suppose a small program customizes the WXML template for element as follows:

<view
  wx:if="{{r.tagName === 'view'}}"
  id="{{r.nodeId}}"
>
  <block  
    wx:for="{{r.c hildren}}"   
    wx:key="nodeId"
  >
      <element data="{{r: item}}" />
  </block>
</view>

<text 
  wx:elif="{{r.tagName === 'text'}}"
>
  {{r.content}}
</text>
Copy the code

Notice that Element references itself recursively in the template and terminates the recursion by conditional judgment. So, when the logical layer passes the following data through setData:

{
  "nodeId": "1"."tagName": "view"."children": [{"nodeId": "2"."tagName": "text", "Content" : "I am?"}, {"nodeId":"3", "tagName": "text"."content": "rax"}}]Copy the code

The resulting view looks like this:

<view>
  <text>I am a</text>
  <text>rax</text>
</view>
Copy the code

In this way, we cleverly implement the ability to dynamically render a view based on incoming setData data while the WXML template is fixed. This is where the run-time solution can be born.

The basic principle of

The runtime solution of Rax is derived from KBone, an isomorphic solution of small programs and Web end officially launched by wechat. The design principle of KBONE can be referred to its official website. A brief summary is to simulate DOM/BOM API in the logical layer, convert these methods of creating views into maintaining a VDOM tree, and then convert it into the corresponding setData data, and finally render the actual view recursively through the preset template. From the DOM API to maintain VDOM tree basic principle is not complicated, the process of the createElement method/the appendChild insertBefore/removeChild corresponds to the basic data structure of the operation.

Those familiar with Rax should know that in order to support cross-ends, Rax hasdriverIn the design. In fact, we can write a driver for the small program side and implement its interface API according to the above principle. However, we ended up using a lower-level simulated BOM/DOM API to complete the entire rendering mechanism. The reasons for doing this are as follows: first, it is the fastest solution based on KBone development. The driver of small program only needs to reuse the driver-DOM of Web side, after all, the bottom layerdocumentwindowThe variables have been modeled; Second, because we want to provide developers with a more web-friendly development experience. This solution means that developers can create views directly using the BOM/DOM API in addition to using JSX, which gives them a little more flexibility. Taking a closer look at the entire commercially available applets runtime framework, Remax connects applets directly from the VDOM layer through the React-Reconciler (similar to the Rax Applets driver design described above), Both KBone and Taro 3.0 chose to simulate a Web environment for rendering. This is also related to the design intentions of the framework developers. The basic schematic diagram of the Rax applet runtime scheme is shown below:

The event system

In the Rax applet runtime, the library that simulates DOM/BOM API is miniapp-Render, which supports the following apis:

In addition to handling render data, another important thing is the event system. It implements a complete event dispatch mechanism through EventTarget base class. Logical layer DOM nodes inherit from EventTarget and collect their own bound events with a unique nodeId. View layer on the template of each built-in component binding nodeId, and monitor all can trigger events, such as a simple view TAB, will bindtap/bindtouchstart/bindtouchend events such as binding. In an event, through the event. The currentTarget. Dataset. Get to the destination node id nodeId, corresponding function to trigger the user on the node binding.

Engineering design

The main engineering process of Rax applet runtime follows the design of Rax Web. JS Bundle packaged by Webpack on the Web side can be reused in the applet runtime. We can inject the window and document variables simulated by miniapp-render into the bundle through the plug-in, and regenerate it into a fixed skeleton of small program project. We can load JS bundle in app.js. Its overall engineering structure is shown in the figure below:

MPA or SPA ?

The above architecture is the result of gradual evolution. Initially, we packaged the run-time applet code using WebPack’s multi-entry mode, where each page was packaged as an entry. This makes the applet behave more like an MPA. The problem with this is that code with common dependencies between pages does not execute in the same memory and does not behave like native applets. This difference led to our final decision to change the project packaging model. The current version of the Rax runtime applet is more spa-like, with all the business code packaged into a SINGLE JS file.

We have modified the link of rax-app package at the entrance of Rax project when the small program is running, which will return the render function of each page according to the route when initialization. The render function creates the root node (the document. The createElement method) mounted to its corresponding Rax components, append to the body and the root node (the document. The body. The appendChild). In the onLoad life cycle of each page, the applet creates a separate Document and sets it as a global variable, and then calls its corresponding render function to render each page independently.

Performance tuning

From the above small program runtime principle, there is a certain gap in its performance compared with the original, which is mainly caused by the following aspects: First, the logical layer runs complete Rax + to process VDOM and generate setData data by simulating DOM/BOM API, which requires more computing time; Second, compared with the native applet, more setData data need to be transmitted. If the data serialization ability of the container layer is weak, the data transmission time will be greatly increased. Third, the view layer dynamically generates views recursively from custom components, and we know that recursive actions are themselves a performance drain. In addition, because the attributes and events that the user needs to bind cannot be known in advance, all the attributes and events can only be pre-bound in the custom component template, which causes many useless events to be fired during the execution of the applet, further aggravating the burden. According to our benchmark calculation, there is about 40% performance gap between the run-time applets framework (including Rax/Taro/Remax, etc.) and native applets on alipay applets platform.

After the release of Rax applet runtime, the performance of Rax applet runtime was significantly lower than that of other runtime frameworks, so we launched a special program for performance tuning. By refactoring in the following areas, we successfully drove the performance of Rax applet runtime applets to industry leading levels, basically on par with Taro/Remax.

  • Update data precision. In the old version, the data of setData is fully updated. Although there is a design of dom subtree segmentation and batch update, there is still a lot of redundancy in data transmission. In the reconstructed version, Rax added node rendering judgment, and did not trigger the update of mounted nodes. All updates are collected to the top root node for unified batch processing, and local updates are realized by accurately calculating the path of data updates. For example, when the class attribute of a node is updated, the setData data might be:

    {
       "root.children.[0].children.[1].class": "active"
    }
    Copy the code
  • The built-in applet component does not need to maintain its property list, but instead assigns values directly based on user input. In the old version, we maintained the attributes of all built-in components, and domNode.getAttribute was called when obtaining the attribute values, which had some performance overhead. The refactoring version of Rax directly assigns values to attributes based on user input and moves the operation of setting default values to the view layer WXS/SJS.

  • Update the data structure in miniapp-Render. After sorting, Rax removed redundant tree data and rewrote getaElementById and other apis. Attribute, classList and other classes are reconstructed. Data structures such as Map and Set are used to improve the overall data processing performance.

  • Render template optimization. In alipay applet, Rax uses template to make recursive calls; In wechat, Rax uses the form of template calling element and then template to avoid the restriction of the number of layers of recursive calling template on wechat. In the template, we try to use template is syntax to judge, reduce a:if/ Wx :if condition judgment, improve the recursive performance of the template.

A mixture of

Whether for the migration of legacy businesses or for performance reasons, there is a need for mixed use in the Rax applet runtime. At present, Rax has opened up the ability to mix with mini – program built-in components, mini – program custom components, mini – program pages and mini – program plug-ins. Of these, using applets to customize components is the most complex.

Use with applets custom components

Customize components in Rax using applets whose import paths need to be consistent with usingComponents (e.g. Import CustomComp from ‘.. / components/CustomComp/index ‘). During compilation, the Rax project uses the Babel plug-in to scan the code, and when it detects that a component used in JSX is a small program custom component (depending on whether there is an AXML file of the same name in its import path), it caches the attributes and events used. It is then dynamically generated into a recursive template through the Webpack plug-in. During the node creation phase of the runtime, the query cache determines whether the node is a custom component. In the case of a custom component, properties in the cache are inserted into its render data and events are bound to the custom component instance.

Mixing with compile-time components (dual engine mixing)

Components produced by Rax applets compilation scheme can be directly regarded as applets custom components in terms of usage form. The Rax project strengthens the link between run-time and compile-time. When using the compile-time component NPM package in the Rax applets runtime, the user does not need to import the specific path of the component, just as with normal components. The Rax project will automatically determine whether this component is a Rax multi-terminal component based on the presence of the miniappConfig field in package.json, and then directly use its compile-time component implementation.

Future optimization direction

Rax is the only small program development solution that supports both compile-time and run-time engines, and its ability to use dual engines can perfectly balance performance and development efficiency. In the future, Rax will implement a more flexible dual-engine hybrid approach, such as the ability to specify a component in a single project for compile-time engine compilation, providing greater flexibility for the business.

conclusion

The above is the principle of Rax small program run time scheme analysis. Run-time solutions address the syntactic limitations inherent in compile-time solutions, but they also have significant performance constraints. It can be said that in the current 2020 node, small program development still does not have the so-called silver bullet, perhaps the fusion of Rax small program twin engine will be a relatively optimal solution within the scope. It’s anyone’s guess how far contrarian mini-programs will go, and developers will still face problems for some time to come. From the perspective of small program development framework, we only hope that all developers can choose the most suitable framework for themselves, and complete the development of small program quickly and efficiently.