[RM] _template vs eval vs sval (v4)

Revision 4 of this 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 = {
	defaultLabel:
    "${Object.values(currentRecord?.lookupsByField)[0][0].field==='HIERARCHY-hierarchy_senior_market_head' ? 'Senior Market Head Unassigned': Object.values(currentRecord?.lookupsByField)[0][0].field==='HIERARCHY-hierarchy_market_head' ? 'Market Head Unassigned': Object.values(currentRecord?.lookupsByField)[0][0].field==='HIERARCHY-hierarchy_sr_region_head' ? 'Senior Region Head Unassigned' : Object.values(currentRecord?.lookupsByField)[0][0].field==='HIERARCHY-hierarchy_regional_head' ? 'Region Head Unassigned':'Business Head Unassigned' }",
    content: "${Date.now() - currentRecord?.start}",
}

const CONTEXT = {
	currentRecord: {
		start: 1736953200000,
		lookupsByField: {
			"HIERARCHY-hierarchy_regional_head": [{
				name: "William Schulz",
          lookupType: "HIERARCHY-hierarchy_regional_head",
          id: "HROLE/AMERICAS_GSA_GVP/OWNERSHIP",
				field: "HIERARCHY-hierarchy_regional_head",
			}]
		}
	}
}

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


function 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;
  }
};

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

const ASTMapCache = new Map();

function 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)) {
        	const scriptToEvaluate = `exports.__output__ = ${iterator.substring(
            2,
            iterator.length - 1
          )}`;

          if (!ASTMapCache.has(scriptToEvaluate)) {
            ASTMapCache.set(
              scriptToEvaluate,
              svalInterpreter.parse(scriptToEvaluate)
            );
          }

          svalInterpreter.run(ASTMapCache.get(scriptToEvaluate));
          
          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) => {
            
                    	const scriptToEvaluate = `exports.__output__ = ${group}`;

          if (!ASTMapCache.has(scriptToEvaluate)) {
            ASTMapCache.set(
              scriptToEvaluate,
              svalInterpreter.parse(scriptToEvaluate)
            );
          }

          svalInterpreter.run(ASTMapCache.get(scriptToEvaluate));

            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;
  }
}

function lodashParseObject(
  templateObject,
  context
){

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

  try {
    return JSON.parse(_.template(JSON.stringify(templateObject))(context)) ??{};
  } catch (e) {
    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.