Skip to content

Excessive calls to this.toString in positionInside, positionBy and rangeBy #1847

@romainmenke

Description

@romainmenke

The intention of these function is to find a position within the string representation of a specific bit of CSS.

So calling this.toString() is important and inevitable.

However these functions call each other but do not pass on the string representation, causing a lot of duplicate work.

This is especially bad in rules.

For rules in particular these functions are called mostly to get positions with selectors, but the entire rule body is also converted to a string representation multiple times, this includes all declarations, nested rules, ...


None of this work is cached/memoized.


Is there anything we can do to reduce the cost of serialization in PostCSS in particular?


A simplistic change that passes on work between these functions already reduces the amount of work :

-  positionInside(index) {
-    let string = this.toString()
+  positionInside(index, stringRepresentation) {
+    let string = stringRepresentation || this.toString()
    let column = this.source.start.column
    let line = this.source.start.line

    for (let i = 0; i < index; i++) {
      if (string[i] === '\n') {
        column = 1
        line += 1
      } else {
        column += 1
      }
    }

    return { line, column }
  }

-  positionBy(opts) {
+  positionBy(opts, stringRepresentation) {
    let pos = this.source.start
    if (opts.index) {
-      pos = this.positionInside(opts.index)
+      pos = this.positionInside(opts.index, stringRepresentation)
    } else if (opts.word) {
-      let index = this.toString().indexOf(opts.word)
-      if (index !== -1) pos = this.positionInside(index)
+      stringRepresentation = this.toString()
+      let index = stringRepresentation.indexOf(opts.word)
+      if (index !== -1) pos = this.positionInside(index, stringRepresentation)
    }
    return pos
  }

  rangeBy(opts) {
    let start = {
      line: this.source.start.line,
      column: this.source.start.column
    }
    let end = this.source.end
      ? {
        line: this.source.end.line,
        column: this.source.end.column + 1
      }
      : {
        line: start.line,
        column: start.column + 1
      }

    if (opts.word) {
-      let index = this.toString().indexOf(opts.word)
+      let stringRepresentation = this.toString()
+      let index = stringRepresentation.indexOf(opts.word)
      if (index !== -1) {
-        start = this.positionInside(index)
-        end = this.positionInside(index + opts.word.length)
+        start = this.positionInside(index, stringRepresentation)
+        end = this.positionInside(index + opts.word.length, stringRepresentation)
      }
    } else {
      if (opts.start) {
        start = {
          line: opts.start.line,
          column: opts.start.column
        }
      } else if (opts.index) {
        start = this.positionInside(opts.index)
      }

      if (opts.end) {
        end = {
          line: opts.end.line,
          column: opts.end.column
        }
      } else if (opts.endIndex) {
        end = this.positionInside(opts.endIndex)
      } else if (opts.index) {
        end = this.positionInside(opts.index + 1)
      }
    }

    if (
      end.line < start.line ||
      (end.line === start.line && end.column <= start.column)
    ) {
      end = { line: start.line, column: start.column + 1 }
    }

    return { start, end }
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions