diff --git a/.yarn/cache/caniuse-lite-npm-1.0.30001516-399d435c2f-044adf3493.zip b/.yarn/cache/caniuse-lite-npm-1.0.30001516-399d435c2f-044adf3493.zip deleted file mode 100644 index 670c47bc..00000000 Binary files a/.yarn/cache/caniuse-lite-npm-1.0.30001516-399d435c2f-044adf3493.zip and /dev/null differ diff --git a/.yarn/cache/caniuse-lite-npm-1.0.30001705-be4a37ca97-42c3b2164b.zip b/.yarn/cache/caniuse-lite-npm-1.0.30001705-be4a37ca97-42c3b2164b.zip new file mode 100644 index 00000000..5681f818 Binary files /dev/null and b/.yarn/cache/caniuse-lite-npm-1.0.30001705-be4a37ca97-42c3b2164b.zip differ diff --git a/.yarn/cache/cheerio-npm-1.0.0-837785b57b-ade4344811.zip b/.yarn/cache/cheerio-npm-1.0.0-837785b57b-ade4344811.zip new file mode 100644 index 00000000..f1be92c1 Binary files /dev/null and b/.yarn/cache/cheerio-npm-1.0.0-837785b57b-ade4344811.zip differ diff --git a/.yarn/cache/cheerio-select-npm-2.1.0-e92bc2f296-843d6d4799.zip b/.yarn/cache/cheerio-select-npm-2.1.0-e92bc2f296-843d6d4799.zip new file mode 100644 index 00000000..e6e7bc62 Binary files /dev/null and b/.yarn/cache/cheerio-select-npm-2.1.0-e92bc2f296-843d6d4799.zip differ diff --git a/.yarn/cache/css-select-npm-5.1.0-9365a79de5-2772c049b1.zip b/.yarn/cache/css-select-npm-5.1.0-9365a79de5-2772c049b1.zip new file mode 100644 index 00000000..5c72f05f Binary files /dev/null and b/.yarn/cache/css-select-npm-5.1.0-9365a79de5-2772c049b1.zip differ diff --git a/.yarn/cache/domutils-npm-3.2.2-290180a284-ae941d56f0.zip b/.yarn/cache/domutils-npm-3.2.2-290180a284-ae941d56f0.zip new file mode 100644 index 00000000..3c1b832a Binary files /dev/null and b/.yarn/cache/domutils-npm-3.2.2-290180a284-ae941d56f0.zip differ diff --git a/.yarn/cache/encoding-sniffer-npm-0.2.0-7e717bfa93-05ad76b674.zip b/.yarn/cache/encoding-sniffer-npm-0.2.0-7e717bfa93-05ad76b674.zip new file mode 100644 index 00000000..f5f04472 Binary files /dev/null and b/.yarn/cache/encoding-sniffer-npm-0.2.0-7e717bfa93-05ad76b674.zip differ diff --git a/.yarn/cache/htmlparser2-npm-9.1.0-5ef8394060-e5f8d51939.zip b/.yarn/cache/htmlparser2-npm-9.1.0-5ef8394060-e5f8d51939.zip new file mode 100644 index 00000000..79276bb9 Binary files /dev/null and b/.yarn/cache/htmlparser2-npm-9.1.0-5ef8394060-e5f8d51939.zip differ diff --git a/.yarn/cache/parse5-htmlparser2-tree-adapter-npm-7.1.0-02959cc692-98326fc544.zip b/.yarn/cache/parse5-htmlparser2-tree-adapter-npm-7.1.0-02959cc692-98326fc544.zip new file mode 100644 index 00000000..3e3e48f3 Binary files /dev/null and b/.yarn/cache/parse5-htmlparser2-tree-adapter-npm-7.1.0-02959cc692-98326fc544.zip differ diff --git a/.yarn/cache/parse5-npm-7.2.1-c48f333f28-11253cf8aa.zip b/.yarn/cache/parse5-npm-7.2.1-c48f333f28-11253cf8aa.zip new file mode 100644 index 00000000..d751f80f Binary files /dev/null and b/.yarn/cache/parse5-npm-7.2.1-c48f333f28-11253cf8aa.zip differ diff --git a/.yarn/cache/parse5-parser-stream-npm-7.1.2-94131559b2-75b232d460.zip b/.yarn/cache/parse5-parser-stream-npm-7.1.2-94131559b2-75b232d460.zip new file mode 100644 index 00000000..047bd10a Binary files /dev/null and b/.yarn/cache/parse5-parser-stream-npm-7.1.2-94131559b2-75b232d460.zip differ diff --git a/.yarn/cache/undici-npm-6.21.2-96736d59b9-4d7227910b.zip b/.yarn/cache/undici-npm-6.21.2-96736d59b9-4d7227910b.zip new file mode 100644 index 00000000..d4f70f7b Binary files /dev/null and b/.yarn/cache/undici-npm-6.21.2-96736d59b9-4d7227910b.zip differ diff --git a/.yarn/cache/whatwg-encoding-npm-3.1.1-7dfe21cf7d-f75a614224.zip b/.yarn/cache/whatwg-encoding-npm-3.1.1-7dfe21cf7d-f75a614224.zip new file mode 100644 index 00000000..13fdfcd7 Binary files /dev/null and b/.yarn/cache/whatwg-encoding-npm-3.1.1-7dfe21cf7d-f75a614224.zip differ diff --git a/.yarn/cache/whatwg-mimetype-npm-4.0.0-ebb293a688-f97edd4b4e.zip b/.yarn/cache/whatwg-mimetype-npm-4.0.0-ebb293a688-f97edd4b4e.zip new file mode 100644 index 00000000..8ef2ab2e Binary files /dev/null and b/.yarn/cache/whatwg-mimetype-npm-4.0.0-ebb293a688-f97edd4b4e.zip differ diff --git a/package.json b/package.json index 33956e9a..afa5991d 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@weknow/gatsby-remark-twitter": "^0.2.3", "babel-plugin-styled-components": "^2.1.4", "block-elements": "^1.2.0", + "cheerio": "^1.0.0", "cross-env": "^7.0.2", "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", @@ -89,7 +90,7 @@ "gatsby-transformer-remark": "^6.11.0", "gatsby-transformer-sharp": "^5.11.0", "js-yaml": "^3.14.0", - "json5": "^2.2.1", + "json5": "^2.2.3", "mdast-util-toc": "^5.0.3", "mime-types": "^2.1.35", "posthtml-parser": "^0.11.0", diff --git a/plugins/gatsby-remark-3perf-transformer/index.js b/plugins/gatsby-remark-3perf-transformer/index.js index 823e8a0a..0eae540e 100644 --- a/plugins/gatsby-remark-3perf-transformer/index.js +++ b/plugins/gatsby-remark-3perf-transformer/index.js @@ -5,6 +5,8 @@ const visitWithAncestors = require('unist-util-visit-parents'); const { parser } = require('posthtml-parser'); const mime = require('mime-types'); const createMediaMarkup = require('./createMediaMarkup'); +const cheerio = require('cheerio'); +const JSON5 = require('json5'); module.exports = ({ markdownAST }) => { // Add a
wrapper around each table @@ -117,9 +119,31 @@ module.exports = ({ markdownAST }) => { }, ); + visit( + markdownAST, + (node) => node.type === 'html' && parseHTMLCodeOptions(node)?.wordWrap, + (node) => { + const $ = cheerio.load(node.value); + const root = $('div.gatsby-highlight'); + + if (root.length === 0) { + throw new Error( + `gatsby-remark-3perf-transformer: Expected a div with class "gatsby-highlight" but got ${ + $.root().children().first().prop('tagName') || 'no element' + } with class "${ + $.root().children().first().attr('class') || 'no class' + }"`, + ); + } + + root.attr('data-word-wrap', ''); + node.value = $.html(); + }, + ); + // Spread lists that have new lines between list items visitWithAncestors(markdownAST, 'list', (node, ancestors) => { - // Don’t do anything with table-of-contents lists + // Don't do anything with table-of-contents lists if (ancestors.some((ancestor) => ancestor.type === 'Toc')) { return; } @@ -133,3 +157,39 @@ module.exports = ({ markdownAST }) => { return markdownAST; }; + +function parseHTMLCodeOptions(node) { + // If you have Markdown like + // ```javascript{wordWrap: true} + // ... + // ``` + // then node.lang will be everything before the space (`javascript{wordWrap:`) and node.meta will be everything after the space (`true`). + // Thus, we need to concatenate these to get the original string (`javascript{wordWrap: true}`). + // This mimics what gatsby-remark-prismjs does: https://github.com/gatsbyjs/gatsby/blob/346314d678d9465c998c12466f810b21592f6cb5/packages/gatsby-remark-prismjs/src/index.js#L38-L48 + if (!node.lang) return; + const language = node.meta ? node.lang + node.meta : node.lang; + if (!language) return; + + const optionsStringWithoutBraces = language.split('{')[1]; + if (!optionsStringWithoutBraces) return; + + const optionsString = '{' + optionsStringWithoutBraces; + + try { + const optionsJson = JSON5.parse(optionsString); + return optionsJson; + } catch (e) { + console.error( + 'gatsby-remark-3perf-transformer: Failed to parse options for a code block:', + e, + ); + console.error('gatsby-remark-3perf-transformer: Original node:', node); + console.error( + 'gatsby-remark-3perf-transformer: Options string:', + optionsString, + ); + throw new Error( + `gatsby-remark-3perf-transformer: Failed to parse options for a code block: ${e.message}. See the error message above for more details.`, + ); + } +} diff --git a/src/components/BlogFooterAccordion/index.tsx b/src/components/BlogFooterAccordion/index.tsx index dc3b22ee..b0ce5045 100644 --- a/src/components/BlogFooterAccordion/index.tsx +++ b/src/components/BlogFooterAccordion/index.tsx @@ -10,15 +10,9 @@ const stuffToPromote = [ href: '/talks/web-perf-101/', }, { - imageKey: 'content', - title: 'Case studies, guides, and open-source tools', - href: '/content', - }, - - { - imageKey: 'services', - title: 'Make your site or web app fast', - href: '/#services', + imageKey: 'reactConcurrency', + title: 'React Concurrency, Explained', + href: '/talks/react-concurrency/', }, ]; @@ -36,13 +30,8 @@ const BlogFooterAccordion = ({ className }: BlogFooterAccordionProps) => { gatsbyImageData(height: 130, placeholder: NONE, layout: FIXED) } } - services: file(relativePath: { eq: "BlogFooterAccordion/services.png" }) { - childImageSharp { - gatsbyImageData(height: 130, placeholder: NONE, layout: FIXED) - } - } - content: file( - relativePath: { eq: "BlogFooterAccordion/content-views.png" } + reactConcurrency: file( + relativePath: { eq: "BlogFooterAccordion/react-concurrency.png" } ) { childImageSharp { gatsbyImageData(height: 130, placeholder: NONE, layout: FIXED) diff --git a/src/components/BlogFooterAccordion/react-concurrency.png b/src/components/BlogFooterAccordion/react-concurrency.png new file mode 100644 index 00000000..7dfeb5f4 Binary files /dev/null and b/src/components/BlogFooterAccordion/react-concurrency.png differ diff --git a/src/components/BlogFooterAccordion/services.png b/src/components/BlogFooterAccordion/services.png deleted file mode 100644 index 9f8a7970..00000000 Binary files a/src/components/BlogFooterAccordion/services.png and /dev/null differ diff --git a/src/components/HomeLeftRightWrapper/index.tsx b/src/components/HomeLeftRightWrapper/index.tsx deleted file mode 100755 index 37301bb9..00000000 --- a/src/components/HomeLeftRightWrapper/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { JSXChildrenProp } from '../../types'; -import { Left, Right, Wrapper } from './styled'; - -interface HomeLeftRightWrapperProps { - alwaysVertical?: boolean; - left: JSXChildrenProp; - right: JSXChildrenProp; - className?: string; -} - -const HomeLeftRightWrapper = ({ - left, - right, - className = '', - alwaysVertical = false, -}: HomeLeftRightWrapperProps) => ( - - {left} - {right} - -); - -export default HomeLeftRightWrapper; diff --git a/src/components/HomeLeftRightWrapper/styled.ts b/src/components/HomeLeftRightWrapper/styled.ts deleted file mode 100755 index 974ecc31..00000000 --- a/src/components/HomeLeftRightWrapper/styled.ts +++ /dev/null @@ -1,27 +0,0 @@ -import styled, { css } from 'styled-components'; -import media from '../../styles/media'; - -export const Left = styled.div``; - -export const Right = styled.div``; - -export const Wrapper = styled.section<{ - $alwaysVertical: boolean; -}>` - ${(props) => - !props.$alwaysVertical && - css` - ${media.notSmall` - display: flex; - - ${Left} { - flex: 4; - margin: 0 60px 24px 0; - } - - ${Right} { - flex: 7; - } - `} - `} -`; diff --git a/src/components/HomeSectionAbout/factoid-bg.svg b/src/components/HomeSectionAbout/factoid-bg.svg deleted file mode 100644 index 2d0b8fd5..00000000 --- a/src/components/HomeSectionAbout/factoid-bg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/HomeSectionAbout/index.tsx b/src/components/HomeSectionAbout/index.tsx deleted file mode 100755 index 036d9592..00000000 --- a/src/components/HomeSectionAbout/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { graphql, useStaticQuery } from 'gatsby'; -import { GraphqlImage } from '../../types'; -import Section, { SectionKind } from '../Section'; -import { - Intro, - Image, - Link, - LinkWrapper, - Primary, - Links, - Wrapper, - Factoid, - Number, -} from './styled'; - -interface AboutSectionData { - talkImage: GraphqlImage; -} - -interface AboutSectionProps { - data: AboutSectionData; -} - -const AboutSection = ({ data }: AboutSectionProps) => ( -
- - - - PerfPerfPerf is ran by{' '} - Ivan Akulov, a{' '} - - Google Developer Expert - - , an international speaker, - and a web performance consultant. - - - - - - Twitter - - - Open-source work - - - 400K reads of Ivan’s perf guides - - - -
-); - -const AboutSectionWithQuery = () => { - const data: AboutSectionData = useStaticQuery(graphql` - { - talkImage: file( - relativePath: { eq: "HomeSectionAbout/talk_cropped.jpg" } - ) { - childImageSharp { - gatsbyImageData(width: 727, placeholder: NONE, layout: CONSTRAINED) - } - } - } - `); - return ; -}; - -export default AboutSectionWithQuery; diff --git a/src/components/HomeSectionAbout/styled.ts b/src/components/HomeSectionAbout/styled.ts deleted file mode 100644 index db7f6874..00000000 --- a/src/components/HomeSectionAbout/styled.ts +++ /dev/null @@ -1,67 +0,0 @@ -import styled from 'styled-components'; -import media from '../../styles/media'; -import { gridSize } from '../../styles/variables'; -import _Image from '../Image'; -import _Link from '../Link'; -import factoidBackground from './factoid-bg.svg'; - -export const Intro = styled.div` - margin-bottom: ${gridSize * 2}px; -`; - -export const Image = styled(_Image)` - max-width: 600px; - display: block; -`; - -export const Primary = styled.div``; -export const Links = styled.div` - ${media.small` - display: none; - `} -`; - -export const Wrapper = styled.div` - ${media.notSmall` - display: flex; - - ${Primary} { - flex: 7; - margin: 0 60px 24px 0; - } - - ${Links} { - flex: 4; - } - `} -`; - -export const LinkWrapper = styled.div` - margin-bottom: ${gridSize}px; -`; - -export const Link = styled(_Link)``; - -export const Factoid = styled.div` - width: 150px; - height: 150px; - margin-top: ${gridSize * 3}px; - padding: 30px; - border-radius: 50%; - background: url(${factoidBackground}); - background-size: cover; - background-position: center; - - display: flex; - text-align: center; - justify-content: center; - align-items: center; - flex-direction: column; - - font-size: 12px; -`; - -export const Number = styled.div` - font-weight: 900; - font-size: 32px; -`; diff --git a/src/components/HomeSectionClients/appsmith.svg b/src/components/HomeSectionClients/appsmith.svg deleted file mode 100644 index 3d129299..00000000 --- a/src/components/HomeSectionClients/appsmith.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/components/HomeSectionClients/castor.svg b/src/components/HomeSectionClients/castor.svg deleted file mode 100644 index 27e58663..00000000 --- a/src/components/HomeSectionClients/castor.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/components/HomeSectionClients/common.svg b/src/components/HomeSectionClients/common.svg deleted file mode 100644 index 89e2be35..00000000 --- a/src/components/HomeSectionClients/common.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/src/components/HomeSectionClients/fat-llama.svg b/src/components/HomeSectionClients/fat-llama.svg deleted file mode 100755 index 1854ad8e..00000000 --- a/src/components/HomeSectionClients/fat-llama.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - diff --git a/src/components/HomeSectionClients/finder.svg b/src/components/HomeSectionClients/finder.svg deleted file mode 100644 index 062a5fe6..00000000 --- a/src/components/HomeSectionClients/finder.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/HomeSectionClients/framer.svg b/src/components/HomeSectionClients/framer.svg deleted file mode 100644 index 32f346f4..00000000 --- a/src/components/HomeSectionClients/framer.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/src/components/HomeSectionClients/google.svg b/src/components/HomeSectionClients/google.svg deleted file mode 100755 index 1ee9372e..00000000 --- a/src/components/HomeSectionClients/google.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/src/components/HomeSectionClients/hugo.svg b/src/components/HomeSectionClients/hugo.svg deleted file mode 100644 index 1aa9b66b..00000000 --- a/src/components/HomeSectionClients/hugo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/components/HomeSectionClients/index.tsx b/src/components/HomeSectionClients/index.tsx deleted file mode 100755 index e0f4d3e2..00000000 --- a/src/components/HomeSectionClients/index.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import Section, { SectionKind } from '../Section'; -import { - AppsmithLogo, - CasesItemsContainer, - CastorLogo, - Content, - FinderLogo, - FramerLogo, - GoogleLogo, - HugoLogo, - NdaLogo, - LogoText, - Mark, - Tag, - TagContainer, -} from './styled'; - -const ClientsSection = () => ( -
- - - - - - education - - -

- We worked with Google on educating developers about web performance. - This targets the long-term goal of making the web faster. -

- - -
-

- This is some of the highest quality tutorial style documentation on - webpack I’ve come across -

- -
-
- - - - -

- Hugo helps to keep meeting notes - and tasks in one place. -

- - react performance - core web vitals - -
-

- The Hugo team was noticing their React app was slow here and there. - They weren’t sure what was causing that, so they reached out to - PerfPerfPerf for help. -

-

- We identified multiple loading and runtime performance optimizations - and guided Hugo through implementing them. As a result,{' '} - - Hugo’s JS init time decreased 2.5×, the editor input latency - improved 1.7×, - {' '} - and the Lighthouse score (even not being a goal) increased by 23 - points. -

-
-

- We were more than satisfied working with Ivan. Ivan communicated - very well throughout the whole process, and{' '} - - the level of detail in his suggestions and recommendations was - just outstanding - - . -

-
Christian Sampaio, CTO @ Hugo
-
-
- - - - -

- Finder helps 2 million - Australian customers track spending, find deals, and save money. -

- - core web vitals - -
-

- A huge chunk of Finder’s new customers come to Finder from search. - Finder wanted to make the site faster and grow their organic traffic, - and they asked us to help. -

-

- We helped Finder to move Core Web Vitals to green{' '} - for 98% of their pages. As a result,{' '} - Finder’s organic traffic grew 50%, and conversion also - increased. -

-
- - - - -

- Castor is a tech platform - that makes clinical trials easier. -

- - react performance - core web vitals - -
-

- Castor reached out to us when they were building a new product. They - noticed the product’s bundle was too large, realized Castor’s design - system caused this, but weren’t sure how to fix it. -

-

- We jumped in and made the design system load faster. We also optimized - the runtime performance of a few critical components, increasing their - responsiveness from 1..2 to 20..30 FPS. -

-
-

- In just a couple weeks, Ivan significantly improved runtime - performance and tree shaking of our key components. One of our - products’ bundle got 3 times smaller – with{' '} - 10× less JS needed for the initial load. -

- -
-
- - - - -

- Appsmith is a low-code - platform for building internal apps. -

- - react performance - -
-

- With Appsmith, we did three React rendering performance audits. This - helped us to focus on different aspects of app’s UI speed. With audit - recommendations implemented, the app got 1.9…2.8× faster{' '} - across multiple user interactions. -

-
- - - - -

- This customer was an enterprise marketing agency with an undisclosed - name. -

- - lighthouse score - -
-

- PerfPerfPerf worked with an enterprise marketing agency to push its - site’s PageSpeed Insights score from ~50 to ~95. This was - critical for converting customers – before the optimizations, some - customers would see a poor score and churn away. -

-

- We analyzed the site, found every issue affecting the score, figured - out how to solve them, and guided the team through implementing the - solutions. -

-
-

- Ivan’s advice was all exceptionally clear.{' '} - - I really appreciated Ivan including his whole thought process, - {' '} - including what he tried that may not have worked – before arriving - at a final solution. -

-

- - Ivan’s responsiveness was a godsend and a clear cut above any - other consultants - {' '} - we’ve used in the past, and the overall level of organization + - timetable he brought was truly fantastic! -

-
Ryan, Marketing Consultant
-
-
- - - - -

- Framer is an website design tool. -

- - react performance - build performance - core web vitals - -
-

- For Framer, higher performance means happier users and better - retention. -

-

- We worked with Framer to perfect the loading speed of Framer UI and - Framer Sites. At the Framer UI,{' '} - - we reduced Speed Index and First CPU Idle of the UI by 40-45% - - . With Framer Sites, we’ve been working together to improve the Core - Web Vitals and the Lighthouse score. We also improved the performance - of internal tools, to make engineers more productive and save costs. -

-
-

- We’ve been very satisfied by working with Ivan! Ivan helped us to - improve loading performance, runtime performance, and tooling around - it.{' '} - - The quality of his work, approach, documentation, etc. has been - outstanding. - -

- -
-
-
-
-); - -export default ClientsSection; diff --git a/src/components/HomeSectionClients/nda.svg b/src/components/HomeSectionClients/nda.svg deleted file mode 100644 index 81d5b776..00000000 --- a/src/components/HomeSectionClients/nda.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/components/HomeSectionClients/styled.ts b/src/components/HomeSectionClients/styled.ts deleted file mode 100755 index e1c2172e..00000000 --- a/src/components/HomeSectionClients/styled.ts +++ /dev/null @@ -1,102 +0,0 @@ -import styled from 'styled-components'; -import media from '../../styles/media'; -import { sizes, colors, gridSize } from '../../styles/variables'; -import appsmithUrl from './appsmith.svg'; -import castorUrl from './castor.svg'; -import finderUrl from './finder.svg'; -import framerUrl from './framer.svg'; -import googleUrl from './google.svg'; -import hugoUrl from './hugo.svg'; -import ndaUrl from './nda.svg'; - -export const CasesItemsContainer = styled.div` - column-gap: ${6 * gridSize}px; - - /* Keep the font size consistent across desktop and mobile */ - --homepage-font-size-regular: ${sizes.fontDefault}px; - --homepage-font-size-small: ${sizes.fontSmall}px; - font-size: var(--homepage-font-size-regular); - - ${media.small` - columns: unset; - `} - - ${media.notSmall` - columns: 2; - `} -`; - -export const Content = styled.div` - margin-bottom: ${9 * gridSize}px; - break-inside: avoid; - page-break-inside: avoid; - - ${media.small` - &:last-child { - margin-bottom: 0; - } - `} -`; - -export const LogoText = styled.div` - margin-bottom: ${sizes.paragraphSpacing}px; - font-size: var(--homepage-font-size-small); -`; - -const Logo = styled.img` - display: block; - margin: 0 0 12px; - height: 48px; - - ${media.notSmall` - margin-top: 10px; - `}; -`; - -export const TagContainer = styled.div` - margin-top: 8px; - display: flex; - flex-wrap: wrap; - gap: 4px 8px; -`; - -export const Tag = styled.div` - flex: none; - background: ${colors.brightYellow}; - padding: 2px 8px; - border-radius: 2px; -`; - -export const Mark = styled.mark` - background: ${colors.softYellow}; -`; - -export const GoogleLogo = styled(Logo).attrs({ - src: googleUrl, -})` - margin-bottom: 4px; -`; - -export const CastorLogo = styled(Logo).attrs({ - src: castorUrl, -})``; - -export const AppsmithLogo = styled(Logo).attrs({ - src: appsmithUrl, -})``; - -export const HugoLogo = styled(Logo).attrs({ - src: hugoUrl, -})``; - -export const FramerLogo = styled(Logo).attrs({ - src: framerUrl, -})``; - -export const FinderLogo = styled(Logo).attrs({ - src: finderUrl, -})``; - -export const NdaLogo = styled(Logo).attrs({ - src: ndaUrl, -})``; diff --git a/src/components/HomeSectionContact/index.tsx b/src/components/HomeSectionContact/index.tsx deleted file mode 100644 index e490dfe3..00000000 --- a/src/components/HomeSectionContact/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { graphql, useStaticQuery } from 'gatsby'; -import { GraphqlImage } from '../../types'; -import { - PromptContainer, - Container, - Contact, - ContactImage, - Link, -} from './styled'; - -interface ContactSectionData { - iamakulov: GraphqlImage; -} - -interface ContactSectionProps { - className?: string; -} - -const ContactSection = ({ - className = '', - data, -}: ContactSectionProps & { data: ContactSectionData }) => ( -
- - book a call - -

- Interested? We’d be glad to help. Book a call with{' '} - - {' '} - Ivan - - , and we’ll figure out how to get you fast. -

-

- Or, prefer emails? Drop us a message at{' '} - perf@3perf.com. -

-
-
-
-); - -const ContactSectionWithQuery = (props: ContactSectionProps) => { - const data: ContactSectionData = useStaticQuery(graphql` - fragment ContactImage on File { - childImageSharp { - gatsbyImageData( - width: 24 - height: 24 - quality: 75 - placeholder: NONE - layout: FIXED - ) - } - } - - { - iamakulov: file( - sourceInstanceName: { eq: "shared" } - relativePath: { eq: "iamakulov.jpg" } - ) { - ...ContactImage - } - } - `); - - return ; -}; - -export default ContactSectionWithQuery; diff --git a/src/components/HomeSectionContact/styled.tsx b/src/components/HomeSectionContact/styled.tsx deleted file mode 100755 index 1c0236b9..00000000 --- a/src/components/HomeSectionContact/styled.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import styled from 'styled-components'; -import media from '../../styles/media'; -import { gridSize } from '../../styles/variables'; -import Image, { ImageProps } from '../Image'; - -export const Container = styled.div` - margin: 0 -60px; - max-width: 900px; - padding: 30px 100px 40px 60px; - border-radius: 4px; - background: #ffdb01; - color: #000; - - --link-color: black; - --link-border-color: rgba(0, 0, 0, 0.5); -`; - -export const Link = styled.a` - font-size: 84px; - font-weight: bold; - white-space: nowrap; - border-bottom: none; - text-decoration: underline; - - ${media.small` - font-size: calc(10vw); - `}; -`; - -export const PromptContainer = styled.div` - margin-top: ${gridSize * 3}px; -`; - -export const Contact = styled.span` - white-space: nowrap; -`; - -export const ContactImage = (props: ImageProps) => ( - -); diff --git a/src/components/HomeSectionMaterials/index.tsx b/src/components/HomeSectionMaterials/index.tsx deleted file mode 100755 index 6c876809..00000000 --- a/src/components/HomeSectionMaterials/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { graphql, useStaticQuery } from 'gatsby'; -import { GraphqlImage } from '../../types'; -import HomeLeftRightWrapper from '../HomeLeftRightWrapper'; -import Link from '../Link'; -import Section, { SectionKind } from '../Section'; -import { - Image, - LinkWrapper, - LinkBlock, - LinkDescription, - LinkText, -} from './styled'; - -interface MaterialsSectionData { - webPerf101: GraphqlImage; -} - -interface MaterialsSectionProps { - data: MaterialsSectionData; -} - -const MaterialsSection = ({ data }: MaterialsSectionProps) => ( -
- - - Web Performance 101 - - A comprehensive guide into modern web performance - - - } - right={ - <> - - - Case study: Analyzing Notion app performance - - - How to make a React app load ~30% faster – by tuning some configs - and delaying some scripts - - - - - Preload, prefetch and other <link> tags - - - HTML has as much as 5 different tags to preload something. This is - a detailed deep-dive into each of them - - - - - How to load polyfills only when needed - - - We’ve been asked: “These days, how do you typically serve - polyfills only to browsers that need them?” - - - - More → - - - } - /> -
-); - -const MaterialsSectionWithQuery = () => { - const data: MaterialsSectionData = useStaticQuery(graphql` - { - webPerf101: file( - relativePath: { eq: "HomeSectionMaterials/web-perf-101.png" } - ) { - childImageSharp { - gatsbyImageData(width: 600, placeholder: NONE, layout: CONSTRAINED) - } - } - } - `); - return ; -}; - -export default MaterialsSectionWithQuery; diff --git a/src/components/HomeSectionMaterials/styled.ts b/src/components/HomeSectionMaterials/styled.ts deleted file mode 100644 index 3c4805d6..00000000 --- a/src/components/HomeSectionMaterials/styled.ts +++ /dev/null @@ -1,45 +0,0 @@ -import styled from 'styled-components'; -import media from '../../styles/media'; -import { linkStyles, linkActiveStyles } from '../../styles/shared-styles'; -import { gridSize } from '../../styles/variables'; -import GatsbyImage from '../Image'; -import Link from '../Link'; - -export const LinkWrapper = styled.div` - margin-bottom: ${gridSize}px; -`; - -export const Image = styled(GatsbyImage)` - /* Because gatsby-image uses inline styles */ - display: block !important; - margin-bottom: ${gridSize}px; - - ${media.small` - max-width: 250px; - `} -`; - -export const LinkText = styled.span``; - -export const LinkBlock = styled(Link)` - color: inherit; - display: block; - border: none; - margin-bottom: ${gridSize}px; - - ${LinkText} { - ${linkStyles} - } - - &:hover, - &:focus, - &:active { - ${LinkText} { - ${linkActiveStyles} - } - } -`; - -export const LinkDescription = styled.div` - font-size: var(--homepage-font-size-small); -`; diff --git a/src/components/HomeSectionMaterials/web-perf-101.png b/src/components/HomeSectionMaterials/web-perf-101.png deleted file mode 100644 index c68c721c..00000000 Binary files a/src/components/HomeSectionMaterials/web-perf-101.png and /dev/null differ diff --git a/src/components/HomeSectionServices/audit-desktop.png b/src/components/HomeSectionServices/audit-desktop.png deleted file mode 100755 index bf570e65..00000000 Binary files a/src/components/HomeSectionServices/audit-desktop.png and /dev/null differ diff --git a/src/components/HomeSectionServices/audit-mobile.png b/src/components/HomeSectionServices/audit-mobile.png deleted file mode 100644 index 3df75311..00000000 Binary files a/src/components/HomeSectionServices/audit-mobile.png and /dev/null differ diff --git a/src/components/HomeSectionServices/david.jpg b/src/components/HomeSectionServices/david.jpg deleted file mode 100644 index e761630f..00000000 Binary files a/src/components/HomeSectionServices/david.jpg and /dev/null differ diff --git a/src/components/HomeSectionServices/index.tsx b/src/components/HomeSectionServices/index.tsx deleted file mode 100755 index 92e8b080..00000000 --- a/src/components/HomeSectionServices/index.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { graphql, useStaticQuery } from 'gatsby'; -import { GraphqlImage } from '../../types'; -import { SectionKind } from '../Section'; -import { - ActionButton, - Column, - ImageText, - DesktopImage, - MobileImageWrapper, - MobileImage, - Note, - Text, - Columns, - H3, - Section, - SvgMask, - Blockquote, - BlockquoteFooter, - BlockquoteImage, - BlockquoteName, - BlockquoteTextWrapper, -} from './styled'; - -interface ServicesSectionProps { - className?: string; - data: { - auditDesktop: GraphqlImage & { - childImageSharp: { gatsbyImageData: { height: number } }; - }; - optimizationDesktop: GraphqlImage & { - childImageSharp: { gatsbyImageData: { height: number } }; - }; - auditMobile: GraphqlImage; - optimizationMobile: GraphqlImage; - workshopMobile: GraphqlImage; - workshopDesktop: GraphqlImage & { - childImageSharp: { fixed: { height: number } }; - }; - davidAvatar: GraphqlImage; - nicolasAvatar: GraphqlImage; - piotrAvatar: GraphqlImage; - }; -} - -const ServicesSection = ({ className = '', data }: ServicesSectionProps) => ( -
- - - - - - - - - - - -

Find What To Improve 🔬

-

- What: we’ll analyze the site, identify all - performance issues, and tell you which ones matter the most & how - to fix them. -

-

- Great when: you need a step-by-step guide into - improving your Core Web Vitals or app responsiveness. -

-
- -

- I want to marry that audit document! The level of detail, - including going through our own codebase and pin-pointing where - exactly improvements need to happen, it’s just... wow -

-
- - - Piotr Krawiec · Product Engineer @ - Framer - -
- - Format: An audit document with concrete actionable - steps · Takes one-two weeks · One month of support afterwards (to make - sure the implementation goes smoothly) - - - Want an example? See these public case studies:{' '} - Notion app ·{' '} - Causal app - -
-
- - - - - - -

Optimize the App 🛠

-

- What: we’ll join your project, roll up our sleeves, - find all performance bottlenecks, and apply precise optimizations – - all by ourselves. -

-

- Great when: you don’t want to distract the team from - delivering business functionality. -

-
- -

- In just a couple weeks, Ivan significantly improved runtime - performance and tree shaking of our key components. One of our - products’ bundle got 3 times smaller – with 10× less JS needed for - the initial load! -

-
- - - - David Sigley - {' '} - · Head of Engineering @ Castor - -
-
-
- - - - - - -

Grow The Team 🧑‍💻

-

- What: we’ll teach your team everything we know about - React performance or Core Web Vitals. -

-

- How this looks: we’ll take a bunch of slow sites or - apps → profile each one → and make each one faster, learning multiple - performance tools and techniques in the process. -

-

- Great when: you want to learn how to keep your site - fast on your own. -

-
- -

- Ivan is immensely knowledgeable in React, in performance and in - the combination of the two. I really recommend PerfPerfPerf’s - React workshop to developers interested in getting the most out of - their apps and in understanding the inner workings of React - profiling. -

-
- - - - Nicolás Delfino - {' '} - · Lead consultant & Performance competence lead @ 13|37 - -
- - Format: Covers React performance or Core Web - Vitals · Online or offline · Takes 12-16 hours.{' '} - More details - -
-
- - -

Open Source 💛

-

- Are you a non-commercial open-source project? Our work wouldn’t be - possible without open-source tools. Reach out, and, if we have - capacity, we’ll be glad to help for free. -

-
- -

Something Else 🎁

-

- Want something custom? We’ll be glad to help. Reach us, and let’s - figure out how our experience can be relevant. -

-
-
- Get a Quote -
-); - -const ServicesSectionWithQuery = () => { - const data: ServicesSectionProps['data'] = useStaticQuery(graphql` - { - auditDesktop: file( - relativePath: { eq: "HomeSectionServices/audit-desktop.png" } - ) { - childImageSharp { - gatsbyImageData(width: 900, placeholder: NONE, layout: FIXED) - } - } - optimizationDesktop: file( - relativePath: { eq: "HomeSectionServices/optimization-desktop.png" } - ) { - childImageSharp { - gatsbyImageData(width: 900, placeholder: NONE, layout: FIXED) - } - } - workshopDesktop: file( - relativePath: { eq: "HomeSectionServices/workshop-desktop.png" } - ) { - childImageSharp { - gatsbyImageData(width: 900, placeholder: NONE, layout: FIXED) - } - } - auditMobile: file( - relativePath: { eq: "HomeSectionServices/audit-mobile.png" } - ) { - childImageSharp { - gatsbyImageData(width: 224, placeholder: NONE, layout: FIXED) - } - } - optimizationMobile: file( - relativePath: { eq: "HomeSectionServices/optimization-mobile.png" } - ) { - childImageSharp { - gatsbyImageData(width: 224, placeholder: NONE, layout: FIXED) - } - } - workshopMobile: file( - relativePath: { eq: "HomeSectionServices/workshop-mobile.png" } - ) { - childImageSharp { - gatsbyImageData(width: 224, placeholder: NONE, layout: FIXED) - } - } - - davidAvatar: file(relativePath: { eq: "HomeSectionServices/david.jpg" }) { - childImageSharp { - gatsbyImageData(height: 20, placeholder: NONE, layout: FIXED) - } - } - - nicolasAvatar: file( - relativePath: { eq: "HomeSectionServices/nicolas.jpg" } - ) { - childImageSharp { - gatsbyImageData(height: 20, placeholder: NONE, layout: FIXED) - } - } - - piotrAvatar: file(relativePath: { eq: "HomeSectionServices/piotr.jpg" }) { - childImageSharp { - gatsbyImageData(height: 20, placeholder: NONE, layout: FIXED) - } - } - } - `); - - return ; -}; - -export default ServicesSectionWithQuery; diff --git a/src/components/HomeSectionServices/nicolas.jpg b/src/components/HomeSectionServices/nicolas.jpg deleted file mode 100644 index 2c2d98de..00000000 Binary files a/src/components/HomeSectionServices/nicolas.jpg and /dev/null differ diff --git a/src/components/HomeSectionServices/optimization-desktop.png b/src/components/HomeSectionServices/optimization-desktop.png deleted file mode 100755 index 69723897..00000000 Binary files a/src/components/HomeSectionServices/optimization-desktop.png and /dev/null differ diff --git a/src/components/HomeSectionServices/optimization-mobile.png b/src/components/HomeSectionServices/optimization-mobile.png deleted file mode 100644 index 53c82698..00000000 Binary files a/src/components/HomeSectionServices/optimization-mobile.png and /dev/null differ diff --git a/src/components/HomeSectionServices/piotr.jpg b/src/components/HomeSectionServices/piotr.jpg deleted file mode 100644 index 3708b18c..00000000 Binary files a/src/components/HomeSectionServices/piotr.jpg and /dev/null differ diff --git a/src/components/HomeSectionServices/styled.ts b/src/components/HomeSectionServices/styled.ts deleted file mode 100755 index 87bdde93..00000000 --- a/src/components/HomeSectionServices/styled.ts +++ /dev/null @@ -1,201 +0,0 @@ -import styled, { css } from 'styled-components'; -import media from '../../styles/media'; -import { gridSize, sizes } from '../../styles/variables'; -import _ActionButton from '../ActionButton'; -import _Image from '../Image'; -import _Section from '../Section'; - -const columnGapVertical = gridSize * 8; -const columnGapHorizontal = gridSize * 8; - -export const Section = styled(_Section)``; - -export const SvgMask = styled.svg` - width: 0; - height: 0; -`; - -export const MobileImageWrapper = styled.div``; - -export const MobileImage = styled(_Image)``; - -export const DesktopImage = styled(_Image)``; - -export const Text = styled.div``; - -interface ImageTextProps { - $direction: 'forward' | 'reverse'; - $desktopImageHeight: number; -} - -export const ImageText = styled.div` - display: flex; - position: relative; - - &:not(:last-child) { - margin-bottom: ${columnGapVertical}px; - } - - ${media.small` - flex-direction: column; - - ${MobileImageWrapper} { - filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.1)); - flex: none; - } - - ${MobileImage} { - clip-path: url(#serviceMobileMask); - } - - ${DesktopImage} { - display: none; - } - - ${Text} { - margin-top: ${gridSize * 3}px; - } - `} - - ${media.notSmall` - min-height: ${(props: ImageTextProps) => props.$desktopImageHeight}px; - - ${MobileImageWrapper} { - display: none; - } - - ${DesktopImage} { - position: absolute; - flex: none; - align-self: flex-start; - - ${(props: ImageTextProps) => - props.$direction === 'forward' && - css` - right: 350px; - margin-right: ${gridSize * 2}px; - `} - - ${(props: ImageTextProps) => - props.$direction === 'reverse' && - css` - left: 350px; - margin-left: ${gridSize * 2}px; - `} - } - - ${Text} { - margin: ${(props: ImageTextProps) => - props.$direction === 'forward' ? '0 0 0 auto' : '0 auto 0 0'}; - margin-top: ${gridSize * 4}px; - width: 350px; - } - `} -`; - -export const Note = styled.p` - ${media.small` - font-size: ${sizes.fontSmall}px; - `} - - ${media.notSmall` - font-size: ${sizes.fontDefault}px; - `} -`; - -export const Columns = styled.div` - ${media.small` - margin-top: ${columnGapVertical}px; - `} - - ${media.notSmall` - display: flex; - margin: 0 -${columnGapHorizontal / 2}px; - `} -`; - -export const Column = styled.div` - ${media.small` - &:not(:last-child) { - margin-bottom: ${columnGapVertical}px; - } - `} - - ${media.notSmall` - flex: 1; - margin: 0 ${columnGapHorizontal / 2}px; - `} -`; - -export const H3 = styled.h3` - margin: 0 0 ${gridSize}px; - font-size: 24px; -`; - -export const ActionButton = styled(_ActionButton)` - margin-top: ${gridSize * 8}px; -`; - -export const Blockquote = styled.blockquote` - border: none; - padding: 0; - - /* Customize the desktop appearance */ - ${media.notSmall` - font-size: var(--homepage-font-size-small); - - footer { - font-size: ${sizes.fontSmall}px; - } - `} -`; - -export const BlockquoteTextWrapper = styled.div` - position: relative; - padding: ${gridSize * 1.5}px ${gridSize * 2}px; - background: white; - border-radius: 8px; - margin-left: -${gridSize * 1.5}px; - - &::after { - content: ''; - position: absolute; - bottom: -12px; - left: ${gridSize * 2}px; - width: 0; - height: 0; - border-top: 12px solid transparent; - border-bottom: 12px solid transparent; - border-left: 12px solid white; - } -`; - -const imageSize = 20; /* px */ -const imageAndNameSpacing = 6; /* px */ -const imageOffsetOutsideContainer = 4; /* 4 */ -export const BlockquoteFooter = styled.footer` - margin-top: ${gridSize * 2.5}px; - margin-left: ${imageSize + - imageAndNameSpacing - - imageOffsetOutsideContainer}px; - - /* Remove the default “—” prefix */ - &::before { - content: none; - } -`; - -export const BlockquoteName = styled.span` - color: black; - display: inline-flex; - align-items: center; -`; - -export const BlockquoteImage = styled(_Image)` - position: absolute; - transform: translateX(-${imageSize + imageAndNameSpacing}px); - flex: none; - width: ${imageSize}px; - height: ${imageSize}px; - border-radius: 50%; -`; diff --git a/src/components/HomeSectionServices/workshop-desktop.png b/src/components/HomeSectionServices/workshop-desktop.png deleted file mode 100644 index fbb16666..00000000 Binary files a/src/components/HomeSectionServices/workshop-desktop.png and /dev/null differ diff --git a/src/components/HomeSectionServices/workshop-mobile.png b/src/components/HomeSectionServices/workshop-mobile.png deleted file mode 100644 index 35f55758..00000000 Binary files a/src/components/HomeSectionServices/workshop-mobile.png and /dev/null differ diff --git a/src/components/HomeSectionTestimonials/appsmith.svg b/src/components/HomeSectionTestimonials/appsmith.svg deleted file mode 100644 index 2efe02c5..00000000 --- a/src/components/HomeSectionTestimonials/appsmith.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/components/HomeSectionTestimonials/castor.svg b/src/components/HomeSectionTestimonials/castor.svg deleted file mode 100644 index 6127b39b..00000000 --- a/src/components/HomeSectionTestimonials/castor.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/HomeSectionTestimonials/common.svg b/src/components/HomeSectionTestimonials/common.svg deleted file mode 100644 index 6a7daec4..00000000 --- a/src/components/HomeSectionTestimonials/common.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/components/HomeSectionTestimonials/fat-llama.svg b/src/components/HomeSectionTestimonials/fat-llama.svg deleted file mode 100644 index c3b4b86f..00000000 --- a/src/components/HomeSectionTestimonials/fat-llama.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/components/HomeSectionTestimonials/finder.svg b/src/components/HomeSectionTestimonials/finder.svg deleted file mode 100644 index d3c5c0f8..00000000 --- a/src/components/HomeSectionTestimonials/finder.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/HomeSectionTestimonials/framer.svg b/src/components/HomeSectionTestimonials/framer.svg deleted file mode 100644 index a266eb9b..00000000 --- a/src/components/HomeSectionTestimonials/framer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/HomeSectionTestimonials/google.svg b/src/components/HomeSectionTestimonials/google.svg deleted file mode 100644 index fe4a2eeb..00000000 --- a/src/components/HomeSectionTestimonials/google.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/components/HomeSectionTestimonials/hugo.svg b/src/components/HomeSectionTestimonials/hugo.svg deleted file mode 100644 index a3fd8af6..00000000 --- a/src/components/HomeSectionTestimonials/hugo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/components/HomeSectionTestimonials/index.tsx b/src/components/HomeSectionTestimonials/index.tsx deleted file mode 100644 index 3018dc72..00000000 --- a/src/components/HomeSectionTestimonials/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import appsmithLogo from './appsmith.svg'; -import castorLogo from './castor.svg'; -import commonLogo from './common.svg'; -import fatLlamaLogo from './fat-llama.svg'; -import finderLogo from './finder.svg'; -import framerLogo from './framer.svg'; -import googleLogo from './google.svg'; -import hugoLogo from './hugo.svg'; -import restreamLogo from './restream.svg'; -import sitepointLogo from './sitepoint.svg'; -import { - Blockquote, - BlockquoteText, - BlockquoteFooter, - Container, - Logo, - Logos, -} from './styled'; -import togglLogo from './toggl.svg'; - -const HomeSectionTestimonials = () => ( - -
- - A ton of extremely useful, actionable feedback that directly and - significantly improved our First Meaningful Paint, Time to Interactive, - [and] Speed Index - - - — Cihat Imamoglu, Senior Software Engineer @{' '} - Fat Llama - -
- - - - - - - - - - - - - -
-); - -export default HomeSectionTestimonials; diff --git a/src/components/HomeSectionTestimonials/restream.svg b/src/components/HomeSectionTestimonials/restream.svg deleted file mode 100644 index 4a742e67..00000000 --- a/src/components/HomeSectionTestimonials/restream.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/components/HomeSectionTestimonials/sitepoint.svg b/src/components/HomeSectionTestimonials/sitepoint.svg deleted file mode 100644 index 2d0b072e..00000000 --- a/src/components/HomeSectionTestimonials/sitepoint.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/components/HomeSectionTestimonials/styled.tsx b/src/components/HomeSectionTestimonials/styled.tsx deleted file mode 100644 index 2a961666..00000000 --- a/src/components/HomeSectionTestimonials/styled.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import styled from 'styled-components'; -import media from '../../styles/media'; -import { colors, gridSize } from '../../styles/variables'; - -export const Container = styled.div``; - -export const Blockquote = styled.blockquote` - /* Reset inherited styles */ - all: unset; - - display: flex; - margin: 0; - - ${media.small` - flex-direction: column; - `} -`; - -export const BlockquoteText = styled.p` - position: relative; - - flex: 3; - margin-right: 36px; - - font-size: 32px; - - &::before { - content: '“'; - position: absolute; - color: ${colors.brightYellow}; - font-size: 120px; - z-index: -1; - font-weight: 900; - top: -50px; - left: -40px; - } - - ${media.small` - margin-right: 0; - font-size: 24px; - `} -`; - -export const BlockquoteFooter = styled.footer` - /* Reset inherited styles */ - all: unset; - - flex: 1; - - ${media.small` - margin-top: 24px; - `} - - &::before { - /* Reset inherited styles */ - all: unset; - } -`; - -export const Logos = styled.div` - display: flex; - flex-wrap: wrap; - gap: ${3 * gridSize}px ${8 * gridSize}px; - - margin-top: ${8 * gridSize}px; - - ${media.small` - gap: ${4 * gridSize}px ${2 * gridSize}px; - `} -`; - -export const Logo = styled.img` - ${media.small` - height: 31px; - width: auto; - `} -`; diff --git a/src/components/HomeSectionTestimonials/toggl.svg b/src/components/HomeSectionTestimonials/toggl.svg deleted file mode 100644 index 7a8be91a..00000000 --- a/src/components/HomeSectionTestimonials/toggl.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/components/Nav/index.tsx b/src/components/Nav/index.tsx index abd4175d..1eccaa3a 100644 --- a/src/components/Nav/index.tsx +++ b/src/components/Nav/index.tsx @@ -3,14 +3,11 @@ import NavBase, { NavBaseProps } from '../NavBase'; const Nav = (props: NavBaseProps) => ( diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx deleted file mode 100755 index 93558a7f..00000000 --- a/src/components/Section/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { JSXChildrenProp } from '../../types'; -import HomeLeftRightWrapper from '../HomeLeftRightWrapper'; -import { Content, H2 } from './styled'; - -interface SectionProps { - children: JSXChildrenProp; - className?: string; - sectionKind?: SectionKind; - title: JSXChildrenProp; -} - -export enum SectionKind { - HORIZONTAL, - VERTICAL, -} - -const Section = ({ - title, - children, - className = '', - sectionKind = SectionKind.HORIZONTAL, -}: SectionProps) => ( - {title}} - right={{children}} - alwaysVertical={sectionKind === SectionKind.VERTICAL} - /> -); - -export default Section; diff --git a/src/components/Section/styled.ts b/src/components/Section/styled.ts deleted file mode 100755 index e38c7412..00000000 --- a/src/components/Section/styled.ts +++ /dev/null @@ -1,16 +0,0 @@ -import styled from 'styled-components'; -import media from '../../styles/media'; - -export const H2 = styled.h2` - margin: 0 0 24px; - font-size: 48px; - line-height: 1.2; - - ${media.small` - font-size: 32px; - `}; -`; - -export const Content = styled.div` - padding-top: 12px; -`; diff --git a/src/content/blog/packer/cover-facebook.jpg b/src/content/blog/packer/cover-facebook.jpg new file mode 100644 index 00000000..b50690f8 Binary files /dev/null and b/src/content/blog/packer/cover-facebook.jpg differ diff --git a/src/content/blog/packer/cover-twitter.jpg b/src/content/blog/packer/cover-twitter.jpg new file mode 100644 index 00000000..f162c0ad Binary files /dev/null and b/src/content/blog/packer/cover-twitter.jpg differ diff --git a/src/content/blog/packer/index.md b/src/content/blog/packer/index.md new file mode 100644 index 00000000..f87c6f81 --- /dev/null +++ b/src/content/blog/packer/index.md @@ -0,0 +1,373 @@ +--- +title: 'Performance Archaeology: Packer.js, a JS Minifier from 2004' +alternativeTitles: + social: 'Performance Archaeology: Packer.js' + seo: 'Performance Archaeology: Packer.js, a JS Minifier from 2004' +author: + id: iamakulov + name: Ivan Akulov + link: https://iamakulov.com + twitterId: iamakulov + facebookId: '100002052594007' +description: 'Reverse-engineering a surprisingly effective JS minifier from 2004' +rssDescription: | + Back in 2018, while doing a performance audit for a client, I stumbled upon an unusually-looking piece of code: + + eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String))... + + This code was weird. Why an eval()? What’s with p,a,c,k,e,r which clearly combines into some name? I couldn’t help but dig into that, and the trip took me all the way back to 2004. +socialImage: + facebook: './cover-facebook.jpg' + twitter: './cover-twitter.jpg' +date: + published: 2025-03-23T20:00:00 + modified: 2025-03-23T20:00:00 +--- + +Back in 2018, when doing a performance audit for a client, I stumbled upon an unusually-looking piece of code: + +```javascript{wordWrap: true} +/*! + * Head JS + * Copyright Tero Piirainen + * License MIT + * Version 1.0.3 + * + * https://github.com/headjs/headjs + */ +eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(newRegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(7(n,t){"1A 1B";7 r(n){a[a.A]=n}7 k(n){m t=31 32(" ?\\\\b"+n+"\\\\b");c.19=c.19.29(t,"")}7 p(n,t){C(m i=0,r=n.A;in?(i.Q.V&&r("V-"+n),i.Q.E&&r("E-"+n)):tt);u.G("1D",f +```javascript{wordWrap: true} +eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('6 4(2){5 3=0;7(5 1=0;1<2.8;++1)3+=2[1];9 3}a.b(4(c,d,e))',15,15,'|i|arguments|result|sum|var|function|for|length|return|console|log|10|20|30'.split('|'),0,{})) +``` + +::: + +What does this code do? Let’s format it: + +```javascript +eval( + (function (p, a, c, k, e, r) { + e = function (c) { + return c.toString(a); + }; + if (!''.replace(/^/, String)) { + while (c--) r[e(c)] = k[c] || e(c); + k = [ + function (e) { + return r[e]; + }, + ]; + e = function () { + return '\\w+'; + }; + c = 1; + } + while (c--) + if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]); + return p; + })( + '6 4(2){5 3=0;7(5 1=0;1<2.8;++1)3+=2[1];9 3}a.b(4(c,d,e))', + 15, + 15, + '|i|arguments|result|sum|var|function|for|length|return|console|log|10|20|30'.split( + '|', + ), + 0, + {}, + ), +); +``` + +and try to understand what each of its parts does: + +- **High-level structure** + + ```javascript + eval( + (function (p, a, c, k, e, r) {...}) + (...) + ) + ``` + + At the high level, the code declares a function (`function (p, a, c, k, e, r)`), immediately calls it with some arguments, and then passes the return value into `eval()`. + + `p`, `a`, `c`, `k`, `e`, and `r` are the function’s arguments, and they receive the following values: + +- **Argument 1 (`p`)** + + ```javascript{wordWrap: true} + /* p = */ '6 4(2){5 3=0;7(5 1=0;1<2.8;++1)3+=2[1];9 3}a.b(4(c,d,e))'; + ``` + + This is the minified version of the original code! Note how + + - all special characters in the original code remain unchanged in the minified code: `for (var i = 0; i < arguments.length; ++i)` becomes `7(5 1=0;1<2.8;++1)` + - but all keywords are replaced with numbers or letters: `function sum(arguments)` becomes `6 4(2)`, and `console.log()` becomes `a.b(...)` + + What do these numbers (`6`, `4`, `2`, etc) and letters (`a`, `b`, etc) correspond to? These seem to come from... + +- **Argument 4 (`k`)** + + ```javascript + /* k = */ "|i|arguments|result|sum|var|function|for|length|return|console|log|10|20|30" + .split("|"), + + // Results in + // ['', 'i', 'arguments', 'result', 'sum', 'var', + // 'function', 'for', 'length', 'return', 'console', + // 'log', '10', '20', '30'] + ``` + + These are keywords extracted from the original code. + + When Packer minifies `function sum(arguments)`, it extracts each keyword, puts it into this array, and replaces each keyword with its index in the array (in [base-62 encoding](https://en.wikipedia.org/wiki/Base62)). So `function sum(arguments)` becomes `6 4(2)`, and `console.log(...)` becomes `a.b(...)`. + + Why is item 0 in this array empty? That’s because number `0` is already used in the original code (`var result = 0`). Keeping that number as-is results in smaller code than replacing it with an index. + +- **Arguments 2 (`a`) and 3 (`c`)** + + ```javascript + /* a = */ 15, + /* c = */ 15, + ``` + + These arguments specify extra details about the keywords array above. + + Argument 2 is the base of the encoding used to index keywords. A few paragraphs above, I said keywords are encoded using base-62 encoding, but that’s not actually true. Technically, the base of the encoding is variable, and can be anything from 1 to 62. In practice, it always equals the number of keywords – unless there are more than 62 keywords, in which case it’s just 62. This is equivalent to fixed base-62 encoding, so I’m not sure why 62 isn’t just hard-coded. + + Argument 3 is the number of all keywords in the array. + +- **Arguments 5 (`e`) and 6 (`r`)** + + ```javascript + /* e = */ 0, + /* r = */ {}, + ``` + + Arguments 5 and 6 are always `0` and `{}`. They’re used to store some intermediate data. + +- **Runtime** + + ```javascript + function (p, a, c, k, e, r) { + e = function (c) { + return c.toString(a); + }; + if (!"".replace(/^/, String)) { + while (c--) r[e(c)] = k[c] || e(c); + k = [ + function (e) { + return r[e]; + }, + ]; + e = function () { + return "\\w+"; + }; + c = 1; + } + while (c--) + if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]); + return p; + } + ``` + + Finally, this is the _runtime_ of Packer. It takes the minified code ("6 4(2){5 3=0;7(5 1=0;1<2.8;++1)3+=2[1];9 3}a.b(4(c,d,e))") and the list of keywords ("|i|arguments|result|sum|var|function|for|length|return|console|log|10|20|30") – and replaces each identifier in the first string with the corresponding keyword from the second string. Once it’s done, it returns the unminified code string: + + ```javascript{wordWrap: true} + 'function sum(arguments){var result = 0;for(var i=0;i +```javascript +const a={name:"Biba",age:3,color:"black"} +const b={name:"Rex",age:4,color:"brown"} +const c={name:"Tweety",age:2,color:"yellow"} +console.log(a,b,c) +``` + +Packer, however, will get you down to this: + +```javascript +eval(function(p,a,c,k,e,r){...}( + '0 7={1:"a",5:3,6:"b"}0 8={1:"c",5:4,6:"d"}0 9={1:"e",5:2,6:"f"}g.h(7,8,9)', + 18, + 18, + 'const|name||||age|color|cat|dog|bird|Biba|black|Rex|brown|Tweety|yellow|console|log'.split('|'), + 0, + {} +)) +``` + +Notice how `const`, `name`, `age`, and `color` (which are repeated over and over in the Terser version) get replaced with a single number in the Packer version? That‘s what ultimately helps the Packer version to be smaller. + +:::sidenote[Of course, it’s not always Gzip – it [could also be Brotli or zstd](https://paulcalvano.com/2024-03-19-choosing-between-gzip-brotli-and-zstandard-compression/). But I’m not getting into the trenches here.] +Does this mean you should use Packer as your minifier of choice? Absolutely not. Every modern properly configured server applies yet another level of compression to any text file it sends. This compression is called Gzip, and it [does the same thing Packer does](https://3perf.com/talks/web-perf-101/#http-brotli-1): deduplicates repeated strings. Except it’s _much_ more effective at that: + +```shell +# Terser +$ cat ./jquery-1.3.2.terser.min.js | wc -c # Size before gzip +55120 +$ cat ./jquery-1.3.2.terser.min.js | gzip-size # Size after gzip +18.6 kB + +# Packer +$ cat ./jquery-1.3.2.packer.min.js | wc -c # Size before gzip +41022 +$ cat ./jquery-1.3.2.terser.min.js | gzip-size # Size after gzip +20.6 kB +``` + +And this is just the size aspect. Packer relies on `eval()`, and `eval()` is terrible because it’s unsafe, [incompatible with Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_eval_expressions), [disables some V8 optimizations](https://groups.google.com/g/v8-users/c/jlISWv1nXWU), and so on. + +# Bonus: Packer is ES2015-Compatible + +Fun fact: Packer is ES2015+-compatible! Packing + +```javascript +function* getAnimals() { + yield { name: 'Biba', age: 3, color: 'black' }; +} + +for await (const animal of getAnimals()) { + console.log(animal); +} +``` + +produces: + +```javascript +eval(function(p,a,c,k,e,r){...}( + '2*0(){4{5:"6",7:3,8:"9"}}a b(c 1 d 0()){e.f(1)}', + 16, + 16, + 'getAnimals|animal|function||yield|name|Biba|age|color|black|for|await|const|of|console|log'.split('|'), + 0, + {} +)) +``` + +which evaluates with no issues. This is surprising for a tool from 2007, and is also pretty amusing given that [it took years](https://github.com/mishoo/UglifyJS/issues/448) for UglifyJS – the then-most-popular minifier – to support ES2015. + +How does Packer support ES2015+? Modern minifiers work by parsing a JavaScript file into [an abstract syntax tree](https://astexplorer.net/). Any new syntax they don’t recognize breaks that process. Packer, however, is incredibly simple; its implementation takes [less than a thousand lines of code](https://github.com/tholu/php-packer/blob/master/src/Packer.php), and the only thing it does is string manipulation. For this tool, any keywords – no matter if from ES3 or from ES2018 – are just words to replace! So as long as you don’t rely on [automatic semicolon insertion](https://en.wikibooks.org/wiki/JavaScript/Automatic_semicolon_insertion) a bit too much: + +```javascript +function* getAnimals() { + yield { name: 'Biba', age: 3, color: 'black' }; +} + +for await (const animal of getAnimals()) { + console.log(animal); +} + +// Gets packed into (roughly) +// +// function*getAnimals(){yield{name:"Biba",age:3,color:"black"}} +// for await(const animal of getAnimals()){console.log(animal)} +// +// and works. +// +// But + +function* getAnimals() { + yield { name: 'Biba', age: 3, color: 'black' }; // no semicolon here + yield { name: 'Rex', age: 4, color: 'brown' }; // no semicolon here either + yield { name: 'Tweety', age: 2, color: 'yellow' }; +} + +for await (const animal of getAnimals()) { + console.log(animal); +} + +// gets packed into (roughly) +// +// function*getAnimals(){yield{name:"Biba",age:3,color:"black"}yield{name:"Rex",age:4,color:"brown"}yield{name:"Tweety",age:2,color:"yellow"}} +// for await(const animal of getAnimals()){console.log(animal)} +// +// and crashes with +// +// Uncaught SyntaxError: Unexpected identifier 'yield' +// +// because you need a `;` after the `yield`s +``` + +...Packer will just work. diff --git a/src/pages/content/background.svg b/src/pages/background.svg similarity index 100% rename from src/pages/content/background.svg rename to src/pages/background.svg diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx deleted file mode 100644 index 018417e7..00000000 --- a/src/pages/content/index.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { graphql } from 'gatsby'; -import GatsbyImage from '../../components/Image'; -import Layout from '../../components/Layout'; -import { LogoKind } from '../../components/Logo'; -import { NavKind } from '../../components/NavBase'; -import WidthWrapper from '../../components/WidthWrapper'; -import { GraphqlImage, JSXChildrenProp } from '../../types'; -import facebookCoverUrl from './facebook-cover.png'; -import { - Background, - Badge, - BadgeImage, - Footer, - Header, - ItemDescription, - ItemImage, - ItemLink, - ItemTitle, - MailchimpSubscribe, - Nav, - Section, - SectionHeader, -} from './styled'; -import twitterCoverUrl from './twitter-cover.png'; - -interface ContentPageProps { - data: { - reactConcurrency: GraphqlImage; - webpackLibs: GraphqlImage; - notion: GraphqlImage; - reexports: GraphqlImage; - polyfills: GraphqlImage; - awesomeWebpackPerf: GraphqlImage; - }; -} - -interface ContentItemProps { - image?: GraphqlImage; - title: string | JSXChildrenProp; - description?: string | JSXChildrenProp; - badge?: string | JSXChildrenProp; - link: string; -} -const ContentItem = ({ - image, - title, - description, - link, - badge, -}: ContentItemProps) => { - return ( - - {image && ( - - - - )} -
- {title} -   - {badge && {badge}} -
- {description && {description}} -
- ); -}; - -export function Head() { - return ( - <> - Talks, articles and tools by PerfPerfPerf - - - - - ); -} - -const ContentPage = (props: ContentPageProps) => { - return ( - - - -