Skip to content

Commit

Permalink
feat(react): performance improvement (#1276)
Browse files Browse the repository at this point in the history
* feat(react): performance improvement

* chore: fix linter

* fix(react): export StyledComponent and HtmlStyledTag
  • Loading branch information
Anber committed Jul 13, 2023
1 parent 418e40a commit 16c057d
Show file tree
Hide file tree
Showing 25 changed files with 846 additions and 137 deletions.
41 changes: 41 additions & 0 deletions .changeset/serious-pans-pump.md
@@ -0,0 +1,41 @@
---
'@linaria/babel-preset': minor
'@linaria/griffel': minor
'@linaria/react': minor
'@linaria/tags': minor
'@linaria/testkit': minor
'@linaria/utils': minor
'@linaria/atomic': minor
'@linaria/cli': minor
'@linaria/core': minor
'@linaria/esbuild': minor
'@linaria/extractor': minor
'@linaria/babel-plugin-interop': minor
'linaria': minor
'@linaria/logger': minor
'@linaria/postcss-linaria': minor
'@linaria/rollup': minor
'@linaria/server': minor
'@linaria/shaker': minor
'@linaria/stylelint': minor
'@linaria/stylelint-config-standard-linaria': minor
'@linaria/vite': minor
'@linaria/webpack-loader': minor
'@linaria/webpack4-loader': minor
'@linaria/webpack5-loader': minor
'linaria-website': minor
---

Breaking Change: Performance Optimization for `styled`

When a component is wrapped in `styled`, Linaria needs to determine if that component is already a styled component. To accomplish this, the wrapped component is included in the list of variables for evaluation, along with the interpolated values used in styles. The issue arises when a wrapped component, even if it is not styled, brings along a substantial dependency tree. This situation is particularly evident when using `styled` to style components from third-party UI libraries.

To address this problem, Linaria will now examine the import location of the component and check if there is an annotation in the `package.json` file of the package containing the components. This annotation indicates whether the package includes other Linaria components. If there is no such annotation, Linaria will refrain from evaluating the component.

Please note that this Breaking Change solely affects developers of component libraries. In order for users to style components from your library, you must include the `linaria.components` property in the library's `package.json` file. This property should have a mask that covers all imported files with components. Here's an example of how to specify it:

```json
"linaria": {
"components": "**/*"
}
```
1 change: 0 additions & 1 deletion packages/babel/package.json
Expand Up @@ -45,7 +45,6 @@
"@linaria/tags": "workspace:^",
"@linaria/utils": "workspace:^",
"cosmiconfig": "^8.0.0",
"find-up": "^5.0.0",
"source-map": "^0.7.3",
"stylis": "^3.5.4"
},
Expand Down
18 changes: 10 additions & 8 deletions packages/babel/src/plugins/preeval.ts
Expand Up @@ -6,14 +6,14 @@ import type { BabelFile, NodePath, PluginObj } from '@babel/core';
import type { Identifier } from '@babel/types';

import { createCustomDebug } from '@linaria/logger';
import type { ExpressionValue } from '@linaria/tags';
import type { StrictOptions } from '@linaria/utils';
import {
JSXElementsRemover,
getFileIdx,
isUnnecessaryReactCall,
nonType,
removeWithRelated,
addIdentifierToLinariaPreval,
} from '@linaria/utils';

import type { Core } from '../babel';
Expand Down Expand Up @@ -80,11 +80,18 @@ export default function preeval(

log('start', 'Looking for template literals…');

const rootScope = file.scope;
this.processors = [];

file.path.traverse({
Identifier: (p) => {
processTemplateExpression(p, file.opts, options, (processor) => {
processor.dependencies.forEach((dependency) => {
if (dependency.ex.type === 'Identifier') {
addIdentifierToLinariaPreval(rootScope, dependency.ex.name);
}
});

processor.doEvaltimeReplacement();
this.processors.push(processor);
});
Expand Down Expand Up @@ -199,22 +206,17 @@ export default function preeval(
dependencies: [],
};

const expressions: ExpressionValue['ex'][] = this.processors.flatMap(
(processor) => processor.dependencies.map((dependency) => dependency.ex)
);

const linariaPreval = file.path.scope.getData('__linariaPreval');
if (!linariaPreval) {
// Event if there is no dependencies, we still need to add __linariaPreval
const linariaExport = t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
t.identifier('exports'),
t.identifier('__linariaPreval')
),
t.objectExpression(
expressions.map((ex) => t.objectProperty(ex, ex, false, true))
)
t.objectExpression([])
)
);

Expand Down
85 changes: 22 additions & 63 deletions packages/babel/src/utils/collectTemplateDependencies.ts
Expand Up @@ -6,16 +6,11 @@
*/

import { statement } from '@babel/template';
import type { NodePath, Scope } from '@babel/traverse';
import type { NodePath } from '@babel/traverse';
import type {
Expression,
ExpressionStatement,
Identifier,
JSXIdentifier,
ObjectExpression,
ObjectProperty,
Program,
SourceLocation,
Statement,
TaggedTemplateExpression,
TemplateElement,
Expand All @@ -26,12 +21,14 @@ import type {
import { cloneNode } from '@babel/types';

import { debug } from '@linaria/logger';
import type { ConstValue } from '@linaria/tags';
import type { ConstValue, FunctionValue, LazyValue } from '@linaria/tags';
import { hasMeta } from '@linaria/tags';
import type { IImport } from '@linaria/utils';
import {
addIdentifierToLinariaPreval,
createId,
findIdentifiers,
mutate,
reference,
referenceAll,
} from '@linaria/utils';

Expand All @@ -41,12 +38,6 @@ import { ValueType } from '../types';
import getSource from './getSource';
import valueToLiteral from './vlueToLiteral';

const createId = (name: string, loc?: SourceLocation | null): Identifier => ({
type: 'Identifier',
name,
loc,
});

function staticEval(
ex: NodePath<Expression>,
evaluate = false
Expand Down Expand Up @@ -161,64 +152,19 @@ function hoistIdentifier(idPath: NodePath<Identifier>): void {
throw unsupported(idPath);
}

function getOrAddLinariaPreval(scope: Scope): NodePath<ObjectExpression> {
const rootScope = scope.getProgramParent();
let object = rootScope.getData('__linariaPreval');
if (object) {
return object;
}

const prevalExport: ExpressionStatement = {
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
object: createId('exports'),
property: createId('__linariaPreval'),
computed: false,
},
right: {
type: 'ObjectExpression',
properties: [],
},
},
};

const programPath = rootScope.path as NodePath<Program>;
const [inserted] = programPath.pushContainer('body', [prevalExport]);
object = inserted.get('expression.right') as NodePath<ObjectExpression>;
rootScope.setData('__linariaPreval', object);
return object;
}

function addIdentifierToLinariaPreval(scope: Scope, name: string) {
const rootScope = scope.getProgramParent();
const object = getOrAddLinariaPreval(rootScope);
const newProperty: ObjectProperty = {
type: 'ObjectProperty',
key: createId(name),
value: createId(name),
computed: false,
shorthand: false,
};

const [inserted] = object.pushContainer('properties', [newProperty]);
reference(inserted.get('value') as NodePath<Identifier>);
}

/**
* Only an expression that can be evaluated in the root scope can be
* used in a Linaria template. This function tries to hoist the expression.
* @param ex The expression to hoist.
* @param evaluate If true, we try to statically evaluate the expression.
* @param addToExport If true, we add the expression to the __linariaPreval.
* @param imports All the imports of the file.
*/
export function extractExpression(
ex: NodePath<Expression>,
evaluate = false,
addToExport = true
addToExport = true,
imports: IImport[] = []
): Omit<ExpressionValue, 'buildCodeFrameError' | 'source'> {
if (
ex.isLiteral() &&
Expand Down Expand Up @@ -282,6 +228,12 @@ export function extractExpression(
referenceAll(inserted);
rootScope.registerDeclaration(inserted);

const exImport = ex.isIdentifier()
? imports.find(
(i) => i.local.node === ex.scope.getBinding(ex.node.name)?.identifier
) ?? null
: null;

// Replace the expression with the _expN() call
mutate(ex, (p) => {
p.replaceWith({
Expand All @@ -298,10 +250,17 @@ export function extractExpression(
// eslint-disable-next-line no-param-reassign
ex.node.loc = loc;

return {
// noinspection UnnecessaryLocalVariableJS
const result: Omit<
LazyValue | FunctionValue,
'buildCodeFrameError' | 'source'
> = {
kind,
ex: createId(expUid, loc),
importedFrom: exImport?.source,
};

return result;
}

/**
Expand Down
29 changes: 7 additions & 22 deletions packages/babel/src/utils/getTagProcessor.ts
Expand Up @@ -10,7 +10,6 @@ import type {
Identifier,
MemberExpression,
} from '@babel/types';
import findUp from 'find-up';

import { BaseProcessor } from '@linaria/tags';
import type {
Expand All @@ -24,6 +23,7 @@ import type { IImport, StrictOptions } from '@linaria/utils';
import {
collectExportsAndImports,
explicitImport,
findPackageJSON,
isNotNull,
mutate,
} from '@linaria/utils';
Expand Down Expand Up @@ -70,26 +70,6 @@ function buildCodeFrameError(path: NodePath, message: string): Error {
}
}

function findPackageJSON(pkgName: string, filename: string | null | undefined) {
try {
const pkgPath = require.resolve(
pkgName,
filename ? { paths: [dirname(filename)] } : {}
);
return findUp.sync('package.json', { cwd: pkgPath });
} catch (er: unknown) {
if (
typeof er === 'object' &&
er !== null &&
(er as { code?: unknown }).code === 'MODULE_NOT_FOUND'
) {
return undefined;
}

throw er;
}
}

const definedTagsCache = new Map<string, Record<string, string> | undefined>();
const getDefinedTagsFromPackage = (
pkgName: string,
Expand Down Expand Up @@ -275,7 +255,12 @@ function getBuilderForIdentifier(
throw buildError(`Unexpected type of an argument ${arg.type}`);
}
const source = getSource(arg);
const extracted = extractExpression(arg, options.evaluate);
const extracted = extractExpression(
arg,
options.evaluate,
false,
imports
);
return {
...extracted,
source,
Expand Down
1 change: 1 addition & 0 deletions packages/griffel/src/processors/makeStyles.ts
Expand Up @@ -29,6 +29,7 @@ export default class MakeStylesProcessor extends BaseProcessor {

const { ex } = callParam[1];
if (ex.type === 'Identifier') {
this.dependencies.push(callParam[1]);
this.#slotsExpName = ex.name;
} else if (ex.type === 'NullLiteral') {
this.#slotsExpName = null;
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Expand Up @@ -62,6 +62,7 @@
"@linaria/core": "workspace:^",
"@linaria/tags": "workspace:^",
"@linaria/utils": "workspace:^",
"minimatch": "^9.0.3",
"react-html-attributes": "^1.4.6",
"ts-invariant": "^0.10.3"
},
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/index.ts
@@ -1,4 +1,9 @@
export { default as styled } from './styled';
export type { StyledJSXIntrinsics, Styled } from './styled';
export type {
HtmlStyledTag,
StyledComponent,
StyledJSXIntrinsics,
Styled,
} from './styled';
export type { CSSProperties } from '@linaria/core';
export type { StyledMeta } from '@linaria/tags';

0 comments on commit 16c057d

Please sign in to comment.