♻️(front) optimize syntax highlighting bundle size

Replace rehype-pretty-code with direct Shiki integration

Migrate syntax highlighting from rehype-pretty-code to @shikijs/rehype for
better bundle optimization. Create a centralized Shiki highlighter that
pre-loads only required languages.

Also remove unused dependencies: @ag-media/react-pdf-table,
@react-pdf/renderer, react-select.

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
This commit is contained in:
Quentin BEY
2026-01-19 14:38:59 +01:00
committed by Laurent Paoletti
parent ff9e8335f2
commit 23964cbf12
8 changed files with 144 additions and 647 deletions
+2
View File
@@ -18,6 +18,7 @@ and this project adheres to
### Changed
- 🏗️(back) migrate to uv
- ♻️(front) optimize syntax highlighting bundle size
## [0.0.12] - 2026-01-27
@@ -45,6 +46,7 @@ and this project adheres to
- ⚰️(back) remove dead code and unused files
- 🐛(back) prevent tool call timeouts
### Removed
- 🔥(chat) remove thinking part from frontend #227
+3 -6
View File
@@ -16,7 +16,6 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@ai-sdk/react": "1.2.12",
"@ai-sdk/ui-utils": "1.2.11",
"@emoji-mart/data": "1.2.1",
@@ -26,8 +25,8 @@
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.18.4",
"@openfun/cunningham-react": "4.0.0",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "9.26.0",
"@shikijs/rehype": "^3.21.0",
"@tanstack/react-query": "5.80.5",
"canvg": "4.0.3",
"clsx": "2.1.1",
@@ -37,7 +36,7 @@
"i18next": "25.2.1",
"i18next-browser-languagedetector": "8.1.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"lodash": "4.17.23",
"lottie-react": "^2.4.1",
"luxon": "3.6.1",
"micromark-extension-llm-math": "3.1.1-20250610",
@@ -49,12 +48,10 @@
"react-i18next": "15.5.2",
"react-intersection-observer": "9.16.0",
"react-markdown": "10.1.0",
"react-select": "5.10.1",
"rehype-katex": "7.0.1",
"rehype-pretty-code": "^0.14.1",
"remark-gfm": "4.0.1",
"remark-math": "6.0.0",
"shiki": "^3.13.0",
"shiki": "^3.21.0",
"styled-components": "6.1.18",
"use-debounce": "10.0.4",
"zod": "^3.25.67",
@@ -1,13 +1,13 @@
import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils';
import { Modal, ModalSize } from '@openfun/cunningham-react';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { use, useCallback, useEffect, useRef, useState } from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { MarkdownHooks } from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import rehypePrettyCode from 'rehype-pretty-code';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
@@ -35,6 +35,9 @@ import { useSourceMetadataCache } from '../hooks';
import { useChatPreferencesStore } from '../stores/useChatPreferencesStore';
import { usePendingChatStore } from '../stores/usePendingChatStore';
import { useScrollStore } from '../stores/useScrollStore';
import { getHighlighter } from '../utils/shiki';
const highlighterPromise = getHighlighter();
// Define Attachment type locally (mirroring backend structure)
export interface Attachment {
@@ -51,7 +54,7 @@ export const Chat = ({
const { t } = useTranslation();
const copyToClipboard = useClipboard();
const { isMobile } = useResponsiveStore();
const highlighter = use(highlighterPromise);
const streamProtocol = 'data'; // or 'text'
const {
@@ -727,9 +730,11 @@ export const Chat = ({
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[
[
rehypePrettyCode,
rehypeShikiFromHighlighter,
highlighter,
{
theme: 'github-dark-dimmed',
fallbackLanguage: 'plaintext',
},
],
rehypeKatex,
@@ -71,10 +71,12 @@ export const CodeBlock = ({ children, ...props }: CodeBlockProps) => {
return (
<>
<CopyCodeButton onCopy={handleCopy} />
<Box ref={preRef} $position="relative" as="pre" {...props}>
{children}
</Box>
<figure data-rehype-pretty-code-figure="">
<CopyCodeButton onCopy={handleCopy} />
<Box ref={preRef} $position="relative" as="pre" {...props}>
{children}
</Box>
</figure>
</>
);
};
@@ -0,0 +1,32 @@
import { createHighlighterCore } from 'shiki/core';
import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
import langBash from 'shiki/langs/bash.mjs';
import langCss from 'shiki/langs/css.mjs';
import langHtml from 'shiki/langs/html.mjs';
import langJavascript from 'shiki/langs/javascript.mjs';
import langJson from 'shiki/langs/json.mjs';
import langMarkdown from 'shiki/langs/markdown.mjs';
import langPython from 'shiki/langs/python.mjs';
import langSql from 'shiki/langs/sql.mjs';
import langTypescript from 'shiki/langs/typescript.mjs';
import langYaml from 'shiki/langs/yaml.mjs';
import themeDimmed from 'shiki/themes/github-dark-dimmed.mjs';
export const getHighlighter = () =>
createHighlighterCore({
themes: [themeDimmed],
langs: [
langJavascript,
langTypescript,
langPython,
langBash,
langJson,
langHtml,
langCss,
langSql,
langYaml,
langMarkdown,
],
engine: createOnigurumaEngine(import('shiki/wasm')),
});
@@ -1,6 +1,7 @@
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { type ReactElement, Suspense } from 'react';
import { Loader } from '@/components';
import { Chat } from '@/features/chat/components/Chat';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -9,7 +10,11 @@ const Page: NextPageWithLayout = () => {
const router = useRouter();
const { id } = router.query;
return <Chat initialConversationId={id as string} />;
return (
<Suspense fallback={<Loader />}>
<Chat initialConversationId={id as string} />
</Suspense>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
@@ -1,11 +1,16 @@
import type { ReactElement } from 'react';
import { type ReactElement, Suspense } from 'react';
import { Loader } from '@/components';
import { Chat } from '@/features/chat/components/Chat';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return <Chat initialConversationId={undefined} />;
return (
<Suspense fallback={<Loader />}>
<Chat initialConversationId={undefined} />
</Suspense>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
+78 -629
View File
File diff suppressed because it is too large Load Diff