_template vs eval vs sval

Benchmark created on


Description

Compare performances when evaluating expressions using the above functions

Preparation HTML

<script scr="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js"></script>

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sval@0.6.3/dist/sval.min.js"></script>

Setup

const TEMPLATE_OBJECT = {
  id: "household_record_page",
  name: null,
  isGlobal: null,
  entityType: "_s_Household",
  grants: [],
  order: null,
  roleIds: null,
  buttons: null,
  createdTime: 1705496417454,
  modifiedTime: 1705496417453,
  children: ["root"],
  allComponents: {
    "0": {
      langVsTranslatedFieldValues: null,
      id: "0",
      templateId: "@sprinklr/widget/Layout",
      persistedId: null,
      name: null,
      order: 0,
      children: ["1", "2", "3"],
      props: {
        pageTemplate: "STICKY_HEADER_TWO_COLS_PINNED_LEFT",
        colGap: 20,
        leftColSpan: 13,
        rightColSpan: 7,
        className: "spr-ui-05 pr-4",
      },
      context: null,
      uiEvents: null,
      formLayout: null,
      tableLayout: null,
      buttons: null,
      valueSource: null,
      evaluatedValue: null,
      translations: null,
    },
    "1": {
      langVsTranslatedFieldValues: null,
      id: "1",
      templateId: "@sprinklr/widget/Layout",
      persistedId: null,
      name: "ONE",
      order: 0,
      children: ["1a", "1b"],
      props: {
        pageTemplate: "ONE_REGION",
      },
      context: null,
      uiEvents: null,
      formLayout: null,
      tableLayout: null,
      buttons: null,
      valueSource: null,
      evaluatedValue: null,
      translations: null,
    },
    root: {
      langVsTranslatedFieldValues: null,
      id: "root",
      templateId: "@sprinklr/widget/Layout",
      persistedId: null,
      name: null,
      order: 0,
      children: ["0"],
      props: {
        pageTemplate: "ONE_REGION",
        stickyHeaderPresent: true,
      },
      context: null,
      uiEvents: null,
      formLayout: null,
      tableLayout: null,
      buttons: null,
      valueSource: null,
      evaluatedValue: null,
      translations: null,
    },
    "1a": {
      langVsTranslatedFieldValues: null,
      id: "1a",
      templateId: "@sprinklr/widget/Layout",
      persistedId: null,
      name: null,
      order: 0,
      children: ["1a1"],
      props: {
        pageTemplate: "ONE_REGION",
        className: "ml-4 pr-4",
      },
      context: null,
      uiEvents: null,
      formLayout: null,
      tableLayout: null,
      buttons: null,
      valueSource: null,
      evaluatedValue: null,
      translations: null,
    },
    "1a1": {
      langVsTranslatedFieldValues: null,
      id: "1a1",
      templateId: "@sprinklr/widget/EntityControls",
      persistedId: null,
      name: "ONE",
      order: 0,
      children: null,
      props: {
        entityType: "_s_Household",
        upfrontActionsCount: 4,
      },
      context: null,
      uiEvents: null,
      formLayout: null,
      tableLayout: null,
      buttons: [
        {
          langVsTranslatedFieldValues: null,
          id: "OPEN_MEETING_RECORD_FORM",
          templateId: "@sprinklr/action/OpenRecordForm",
          persistedId: null,
          label: "Schedule Meeting",
          detail: null,
          icon: "meeting",
          entityType: "_s_Meeting",
          action: null,
          props: {
            type: "ICON_TEXT_BUTTON",
            recordFormVariant: "THIRD_PANE",
            recordFormLayoutId: "finance_meeting",
            title: "Schedule Meeting",
            context: {
              linkedEntityId: "_s_Household-:-${currentRecord?.entityId}",
              attendees: "${currentRecord?.attendees}",
            },
          },
          translations: null,
          children: null,
          visibility: null,
          disabilityFilter: null,
          payload: null,
        },
        {
          langVsTranslatedFieldValues: null,
          id: "OPEN_INTERACTION_RECORD_FORM",
          templateId: "@sprinklr/action/OpenRecordForm",
          persistedId: null,
          label: "Log Interaction",
          detail: null,
          icon: "interaction",
          entityType: "_s_Interaction",
          action: null,
          props: {
            type: "ICON_TEXT_BUTTON",
            recordFormVariant: "THIRD_PANE",
            recordFormLayoutId: "finance_interaction",
            title: "Log Interaction",
            messageOnSuccess: "Successfully created the interaction",
            context: {
              linkedEntityId: "_s_Household-:-${currentRecord?.entityId}",
              attendees: [
                {
                  linkedEntityId:
                    "_s_Customer-:-${currentRecord?.values?._c_headCustomerId}",
                },
              ],
              relatedTo: [
                "_s_Customer-:-${currentRecord?.values?._c_headCustomerId}",
              ],
            },
          },
          translations: null,
          children: null,
          visibility: null,
          disabilityFilter: null,
          payload: null,
        },
        {
          langVsTranslatedFieldValues: null,
          id: "OPEN_SUPERVISOR_REVIEW_RECORD_FORM",
          templateId: "@sprinklr/action/OpenRecordForm",
          persistedId: null,
          label: "Add Review",
          detail: null,
          icon: "interaction",
          entityType: "_c_reviewforsupervisor",
          action: null,
          props: {
            type: "ICON_TEXT_BUTTON",
            recordFormVariant: "THIRD_PANE",
            recordFormLayoutId: "household_review_form",
            title: "Add Review",
            messageOnSuccess: "Successfully created the review",
            context: {
              _c_wealthgroupidforreview: "${currentRecord?.entityId}",
              name: "${currentRecord?.name} Review",
              _c_wealthgroupheadforreview:
                "${currentRecord?.values?._c_headCustomerId}",
              _c_rm_user:
                "${currentRecord?.shares?.jointGrants?.find(e => e?.endsWith('/WRM') && e?.startsWith('USER/'))?.split('/')?.[1]}",
            },
          },
          translations: null,
          children: null,
          visibility: {
            filterType: "AND",
            filters: [],
            filters_dummy: [
              {
                filterType: "EXPRESSION",
                field:
                  "${currentRecord?.shares?.jointGrants?.find(e => e?.endsWith('/WRM') && e?.startsWith('USER/'))?.split('/')?.[1] !== currentUser.userId}",
              },
              {
                filterType: "EXPRESSION",
                field:
                  "${currentRecord?.shares?.jointGrants?.find(e => e?.endsWith('/IA') && e?.startsWith('USER/'))?.split('/')?.[1] !== currentUser.userId}",
              },
              {
                filterType: "EXPRESSION",
                field:
                  "${currentRecord?.shares?.jointGrants?.find(e => e?.endsWith('/SRM') && e?.startsWith('USER/'))?.split('/')?.[1] !== currentUser.userId}",
              },
            ],
          },
          disabilityFilter: null,
          payload: null,
        },
        {
          langVsTranslatedFieldValues: null,
          id: "INITIATE_WORKFLOW",
          templateId: "@sprinklr/action/GuidedAction",
          persistedId: null,
          label: "Initiate Workflow",
          detail: null,
          icon: "workflow",
          entityType: "_s_Household",
          action: "254937",
          props: {
            type: "ICON_TEXT_BUTTON",
          },
          translations: null,
          children: null,
          visibility: null,
          disabilityFilter: null,
          payload: null,
        },
      ],
      valueSource: null,
      evaluatedValue: null,
      translations: null,
    },
  },
};

const CONTEXT = {
  currentRecord: {
    attendees: [{ id: "1", name: "Nik" }],
    values: {
      _c_headCustomerId: "123",
    },
    entityId: "123",
    name: "Nikunj",
    shares: { jointGrants: ["USER/123/WRM"] },
  },
  currentUser: {
    userId: 123,
  },
};


const REGEX_FOR_SINGLE_OR_MULTIPLE_EXP_IN_STRING = /\${([^}]*)}/g;
const REGEX_FOR_SINGLE_EXP_IN_STRING = /^\${.*}$/;


function evalTemplateObject(templateObject, context){
 console.log('evalTemplateObject',templateObject,context);
 
//don't want to allow these things to be accessible while running eval
  const window = undefined;
  const fetch = undefined;
  const document = undefined;
  const setTimeout = undefined;
  const setInterval = undefined;

  const { currentRecord, currentUser, iteratorObject, router, parentRecord, resources, __customContext__ } = context;

  if (_.isEmpty(templateObject)) {
    return templateObject;
  }

  const recursivelyIterate = iterator => {
    //base case
    if (!_.isObject(iterator)) {
      if (typeof iterator === 'string') {
        //for exp like ${currentRecord.name}
        if (REGEX_FOR_SINGLE_EXP_IN_STRING.test(iterator)) {
          const parsedValue = eval(iterator.substring(2, iterator.length - 1));
          
          return parsedValue;
        }

        //for exp like 'Hi ${currentRecord.name} ${currentRecord.surname}'
        return iterator.replace(REGEX_FOR_SINGLE_OR_MULTIPLE_EXP_IN_STRING, (match, group) => {
          const parsedValue = eval(`${group}`);

          return parsedValue;
        });
      }
      return iterator;
    }

    //if iterator is array
    if (Array.isArray(iterator)) {
      return iterator.map(value => recursivelyIterate(value));
    }

    //if iterator is object
    return _.reduce(
      iterator,
      (acc, value, key) => {
        acc[key] = recursivelyIterate(value);
        return acc;
      },
      {}
    );
  };

  try {
    return recursivelyIterate(templateObject);
  } catch (error) {
    return templateObject;
  }
};

function lodashParseObject(
  templateObject,
  context
){
console.log('lodashTemplateObject',templateObject,context,_);
  if (_.isEmpty(templateObject)) {
    return templateObject;
  }

  try {
    return JSON.parse(_.template(JSON.stringify(templateObject))(context)) ??{};
  } catch (e) {
    return templateObject;
  }
};


const svalInterpreter = new Sval({
  ecmaVer: 11,
  sourceType: "script",
  sandBox: true,
});

function svalParseObject(templateObject, context) {
	console.log('svalParseObject',templateObject,context,);
	
  svalInterpreter.import({
    currentRecord: context.currentRecord,
    currentUser: context.currentUser,
    iteratorObject: context.iteratorObject,
    router: context.router,
    parentRecord: context.parentRecord,
    resources: context.resources,
    __customContext__: context.__customContext__,
  });

  if (_.isEmpty(templateObject)) {
    return templateObject;
  }

  const recursivelyIterate = (iterator) => {
    //base case
    if (!_.isObject(iterator)) {
      if (typeof iterator === "string") {
        //for exp like ${currentRecord.name}
        if (REGEX_FOR_SINGLE_EXP_IN_STRING.test(iterator)) {
          svalInterpreter.run(
            `exports.__output__ = ${iterator.substring(2, iterator.length - 1)}`
          );

          return svalInterpreter.exports.__output__;
        }

        //for exp like 'Hi ${currentRecord.name} ${currentRecord.surname}'
        return iterator.replace(
          REGEX_FOR_SINGLE_OR_MULTIPLE_EXP_IN_STRING,
          (match, group) => {
            svalInterpreter.run(`exports.__output__ = ${group}`);

            return svalInterpreter.exports.__output__;
          }
        );
      }
      return iterator;
    }

    //if iterator is array
    if (Array.isArray(iterator)) {
      return iterator.map((value) => recursivelyIterate(value));
    }

    //if iterator is object
    return _.reduce(
      iterator,
      (acc, value, key) => {
        acc[key] = recursivelyIterate(value);
        return acc;
      },
      {}
    );
  };

  try {
    return recursivelyIterate(templateObject);
  } catch (error) {
    return templateObject;
  }
}

Test runner

Ready to run.

Testing in
TestOps/sec
eval
evalTemplateObject(TEMPLATE_OBJECT, CONTEXT);
ready
sval
svalParseObject(TEMPLATE_OBJECT, CONTEXT);
ready
lodash/_template
lodashParseObject(TEMPLATE_OBJECT, CONTEXT);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.