static async audit_(artifacts, networkRecords, context) {
    const ignoreThresholdInBytes =
      context.options?.ignoreThresholdInBytes || IGNORE_THRESHOLD_IN_BYTES;
    const duplication =
      await DuplicatedJavascript._getDuplicationGroupedByNodeModules(artifacts, context);

    /** @type {Map<string, number>} */
    const compressionRatioByUrl = new Map();

    /** @type {Item[]} */
    const items = [];

    let overflowWastedBytes = 0;
    const overflowUrls = new Set();

    /** @type {Map<string, number>} */
    const wastedBytesByUrl = new Map();
    for (const [source, sourceDatas] of duplication.entries()) {
      // One copy of this module is treated as the canonical version - the rest will have
      // non-zero `wastedBytes`. In the case of all copies being the same version, all sizes are
      // equal and the selection doesn't matter (ignoring compression ratios). When the copies are
      // different versions, it does matter. Ideally the newest version would be the canonical
      // copy, but version information is not present. Instead, size is used as a heuristic for
      // latest version. This makes the audit conserative in its estimation.

      /** @type {SubItem[]} */
      const subItems = [];

      let wastedBytesTotal = 0;
      for (let i = 0; i < sourceDatas.length; i++) {
        const sourceData = sourceDatas[i];
        const scriptId = sourceData.scriptId;
        const script = artifacts.Scripts.find(script => script.scriptId === scriptId);
        const url = script?.url || '';
        const compressionRatio = estimateCompressionRatioForContent(
          compressionRatioByUrl, url, artifacts, networkRecords);
        const transferSize = Math.round(sourceData.resourceSize * compressionRatio);

        subItems.push({
          url,
          sourceTransferBytes: transferSize,
        });

        if (i === 0) continue;
        wastedBytesTotal += transferSize;
        wastedBytesByUrl.set(url, (wastedBytesByUrl.get(url) || 0) + transferSize);
      }

      if (wastedBytesTotal <= ignoreThresholdInBytes) {
        overflowWastedBytes += wastedBytesTotal;
        for (const subItem of subItems) {
          overflowUrls.add(subItem.url);
        }
        continue;
      }

      items.push({
        source,
        wastedBytes: wastedBytesTotal,
        // Not needed, but keeps typescript happy.
        url: '',
        // Not needed, but keeps typescript happy.
        totalBytes: 0,
        subItems: {
          type: 'subitems',
          items: subItems,
        },
      });
    }

    if (overflowWastedBytes > ignoreThresholdInBytes) {
      items.push({
        source: 'Other',
        wastedBytes: overflowWastedBytes,
        url: '',
        totalBytes: 0,
        subItems: {
          type: 'subitems',
          items: Array.from(overflowUrls).map(url => ({url})),
        },
      });
    }

    /** @type {LH.Audit.Details.TableColumnHeading[]} */
    const headings = [
      /* eslint-disable max-len */
      {key: 'source', valueType: 'code', subItemsHeading: {key: 'url', valueType: 'url'}, label: str_(i18n.UIStrings.columnSource)},
      {key: null, valueType: 'bytes', subItemsHeading: {key: 'sourceTransferBytes'}, granularity: 10, label: str_(i18n.UIStrings.columnTransferSize)},
      {key: 'wastedBytes', valueType: 'bytes', granularity: 10, label: str_(i18n.UIStrings.columnWastedBytes)},
      /* eslint-enable max-len */
    ];

    // TODO: show warning somewhere if no source maps.
    return {
      items,
      headings,
      wastedBytesByUrl,
    };
  }
Each details item returned is a module with subItems for each resource that
includes it. The wastedBytes for the details item is the number of bytes
occupied by the sum of all but the largest copy of the module. wastedBytesByUrl
attributes the cost of the bytes to a specific resource, for use by lantern.
@param {LH.Artifacts} artifacts
@param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@param {LH.Audit.Context} context
@return {Promise<ByteEfficiencyProduct>}